دعم Natural في Text و TextEditor

كيف تجعل النص في تطبيقك يدعم اتجاه اللغة بدلاً من اتجاه التطبيق
بمعنى جعله .natural اذا محتوى النص كان عربي يكون اتجاه النص RTL واذا كان محتوى النص انجليزي يكون اتجاه النص LTR تعرف على الطريقة في هذا المقال

اذا لاحظت فأن جميع الـ alignment في SwiftUI تعتمد على ثلاثة اتجاهات leading , trailing , center

عكس في UIKit يوجد left , right , center وهنا المشكله !

لتجربة الواجهة الحالية في حال كانت باللغة العربيه RTL او باللغة الانجليزية LTR

struct ContentView: View {

    var body: some View {
        
        ScrollView {
        VStack {
        
            Text("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")
                .padding()

            
            Text("هذا النص هو مثال لنص يمكن أن يستبدل في نفس المساحة، لقد تم توليد هذا النص من مولد النص العربى، حيث يمكنك أن تولد مثل هذا النص أو العديد من النصوص الأخرى إضافة إلى زيادة عدد الحروف التى يولدها التطبيق.إذا كنت تحتاج إلى عدد أكبر من الفقرات يتيح لك مولد النص العربى زيادة عدد الفقرات كما تريد، النص لن يبدو مقسما ولا يحوي أخطاء لغوية، مولد النص العربى مفيد لمصممي المواقع على وجه الخصوص، حيث يحتاج العميل فى كثير من الأحيان أن يطلع على صورة حقيقية لتصميم الموقع.ومن هنا وجب على المصمم أن يضع نصوصا مؤقتة على التصميم ليظهر للعميل الشكل كاملاً،دور مولد النص العربى أن يوفر على المصمم عناء البحث عن نص بديل لا علاقة له بالموضوع الذى يتحدث عنه التصميم فيظهر بشكل لا يليق. هذا النص يمكن أن يتم تركيبه على أي تصميم دون مشكلة فلن يبدو وكأنه نص منسوخ، غير منظم، غير منسق، أو حتى غير مفهوم. لأنه مازال نصاً بديلاً ومؤقتاً.")
                .padding()

            Text("Try one line of text")
            .padding()

            Text("تجربة سطر واحد من النص")
            .padding()

            Spacer()
        }
        }
        .environment(\.locale, .init(identifier: "ar"))
        .environment(\.layoutDirection, .rightToLeft)

    }

}

الكود السابق، راح تلاحظ وجود

        .environment(\.locale, .init(identifier: "ar"))
        .environment(\.layoutDirection, .rightToLeft)

هذه فقط لجعل الواجهه تستخدم ملف الترجمة العربية واجبارها تكون RTL بدون الحاجة اني اغير لغة الجهاز (من اجل التجربة فقط)

اذا لاحظت النص لما يكون سطر واحد يكون في المنتصف في SwiftUI هذا هو الافتراضي

كثير من المطورين يغلطوا ويضيفوا الـText داخل HStack ويستخدموا Spacer() لكن هذه الطريقة خاطئه

الطريقة الصحيحة لجعلها تعتمد على لغة التطبيق (الجهاز) هي بهذا الشكل

            Text("Try one line of text")
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding()

            Text("تجربة سطر واحد من النص")
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding()

من الفقرات السابقة اتوقع عرفت المشكلة بسبب اعتماد SwiftUI على
leading , trailing تعتمد على اتجاه لغة النظام

بما يعني اذا لغة الجهاز انجليزية LTR
leading تعني يسار و trailing تعني يمين

والعكس اذا لغة الجهاز عربية RTL
leading تعني يمين و trailing تعني تعني يسار

على ايام SwiftUI 2 و iOS 14 كنت اعتمد على UILabel بمعنى استخدم العنصر من UIKit و واستخدم في SwiftUI
والسبب لانه استطيع احل المشكله السابقه به وايضا استطيع استخدم NSAttributedString التي تعطيني امكانية اتحكم في النص بتغيير لون نص معين ، اجعل جزء من النص Bold او غيره

Apple في iOS 15 ولغة Swift 5.5 اعادة بناء الـ NSAttributedString بما يعني صار 100% بلغة Swift
وبالتالي جعلت SwiftUI يعتمد عليه 100% دون الحاجة الى الاعتماد على UIKit

بس زي ما يقولوا الحلو ما يكمل =) الـ paragraphStyle الي من خلاله تقدر تختار .natrual وتخلي النص يعتمد على لغته في تحديد اتجاه غير مدعوم في SwiftUI

مع ذلك شفت انه مع دعم SwiftUI لـ AttributedString وايضا ميزة Markdown مافي حاجة اني اعتمد على UILabel فلابد من وجود طريقة اخرى لدعم .natural بحيث يكون 100% SwiftUI

بعد البحث والتجربة والخطأ وجدت طريقة فعاله وصحيحه 100%

كما ذكرت سابقا بأنه
اذا لغة الجهاز انجليزية LTR
leading تعني يسار و trailing تعني يمين

والعكس اذا لغة الجهاز عربية RTL
leading تعني يمين trailing وتعني تعني يسار

اذا اقدر اعرف اتجاه التطبيق (لغة التطبيق)
وايضا اتجاه النص فاقدر بكل سهولة أن اجعل النص natural

المشكلة الاولى سهله حلها بالاعتماد على layoutDirection
باستخدامه راح اعرف اتجاه التطبيق (لغة التطبيق)

اما تجاه الواجهة يمين الى يسار RTL او يسار الى يمين LTR

    @Environment(\.layoutDirection) private var layoutDirection

لكن كيف اعرف لغة النص ؟
ايضا بسيطه بالاعتماد على Framework NaturalLanguage ابل اطلقته في iOS 12

الـ NaturalLanguage يحتوي على كثير من المميزات ولكننا هنا راح نعتمد على ميزة وحده وهي معرفة اتجاه النص

دمج الاثنين مع بعض راح نجعل النص natural بحيث اتجاه النص يعتمد فقط على اتجاهه دون الاهتمام بإتجاه التطبيق

لذا انشاء ملف جديدة نوعه SwiftUI وبإسم NaturalText

في البداية فقط اضيف متغير text مافي حاجة لجعله State لانه قيمته لن تتغير

واضيف هذه السطرين

ايش الفائدة من استخدامهم ؟ السطر الاول راح يستخدم في حال كان النص عباره عن سطر واحد الثاني راح يستخدم اذا كان النص مكون من عدة اسطر

struct NaturalTextView: View {
    @Environment(\.layoutDirection) private var layoutDirection

    var text : String

    var body: some View {
            Text(text)
            .frame(maxWidth: .infinity, alignment: .trailing)
            .multilineTextAlignment(naturalTextAlignment)
    }
}

حاليا النص دائما راح يكون حسب اتجاه التطبيق (لغة التطبيق)

لذا في البداية سوف نضيف

import NaturalLanguage

وايضا layoutDirection

import SwiftUI
import NaturalLanguage

struct NaturalTextView: View {
    @Environment(\.layoutDirection) private var layoutDirection
}

بعدها راح نضيف هذا الكود
هذا الكود يعتمد على NaturalLanguage ولاحظ مررت له النص text

    private var dominantLanguage: String? {
        
        let firstChar = "\(text.first ?? " ")"
        if #available(iOS 12, *) {
           return NLLanguageRecognizer.dominantLanguage(for: firstChar)?.rawValue
        } else {
            return NSLinguisticTagger.dominantLanguage(for: firstChar)
        }
    }

في هذه الخطوه راح نسوي متغير جديد بإسم naturalAlignment ونوعه Alignment

لاحظ في البداية استخدمنا المتغير السابق dominantLanguage في حال ما تعرف على اللغة بمعنى كانت رموز او ارقام مثلا راح نخلي الاتجاه حسب لغة التطبيق leading

     private var naturalAlignment: Alignment {
        guard let dominantLanguage = dominantLanguage else {
            // If we can't identify the strings language, use the system language's natural alignment
            return .leading
        }

        switch NSParagraphStyle.defaultWritingDirection(forLanguage: dominantLanguage) {
        case .leftToRight:
            if layoutDirection == .rightToLeft {
                return .trailing
            } else {
                return .leading
            }
           
        case .rightToLeft:
            if layoutDirection == .leftToRight {
                return .trailing
            } else {
                return .leading
            }
        case .natural:
            return .leading
            
        @unknown default:
            return .leading
        }
    }

بعدها استخدمنا

NSParagraphStyle.defaultWritingDirection

بالاعتماد على اللغة راح يعرف اتجاه النص مثلا كانت قيمة dominantLanguage
“ar” بما يعني لغة عربية بالاعتماد على NSParagraphStyle.defaultWritingDirection
راح يرجع .rightToLeft

الجزء الاخير من الكود السابق قد يكون محير ولكن ركز الـ case هو اتجاه النص

وبداخل اعتمدت على layoutDirection مثلا اذا كان اتجاه النص leftToRight بما يعني نص انجليزي في حال كان لغة الجهاز عربي راح يكون اليسار trailing لكن اذا لغة الجهاز لغة انجليزي راح يكون leading

نفس الامر في حال كان لغة الجهاز عربي والنص عربي راح يكون اتجاه النص يمين leading واذا النص انجليزي راح يكون الاتجاه يسار trailing

الان راح نحتاج نكرر نفس الكود ولكن في متغير اخر باسم naturalTextAlignment ونوعه TextAlignment

الفرق بين هذا والسابق ، هذا naturalTextAlignment راح يكون للنص الي يحتوي على عدة اسطر
لكن naturalAlignment للنص الي يكون عباره عن سطر واحد فقط

   private var naturalTextAlignment: TextAlignment {
        guard let dominantLanguage = dominantLanguage else {
            // If we can't identify the strings language, use the system language's natural alignment
            return .leading
        }
        
        
        switch NSParagraphStyle.defaultWritingDirection(forLanguage: dominantLanguage) {
        case .leftToRight:
            if layoutDirection == .rightToLeft {
                return .trailing
            } else {
                return .leading
            }
           
        case .rightToLeft:
            if layoutDirection == .leftToRight {
                return .trailing
            } else {
                return .leading
            }
        case .natural:
            return .leading
        @unknown default:
            return .leading
        }
    }

الخطوه الاخيره نحتاج نستخدم المتغيرين

            Text(text)
            .frame(maxWidth: .infinity, alignment: naturalAlignment)
            .multilineTextAlignment(naturalTextAlignment)

الكود النهائي

////
//NaturalText.swift
//NaturalText
//
//Created by Basel Baragabah on 18/02/2022.
//Copyright © 2022 Basel Baragabah. All rights reserved.
//

import SwiftUI
import NaturalLanguage

struct NaturalText: View {
    @Environment(\.layoutDirection) private var layoutDirection

    var text : String

    var body: some View {
            Text(text)
            .frame(maxWidth: .infinity, alignment: naturalAlignment)
            .multilineTextAlignment(naturalTextAlignment)
    }
    
    private var naturalAlignment: Alignment {
        guard let dominantLanguage = dominantLanguage else {
            // If we can't identify the strings language, use the system language's natural alignment
            return .leading
        }
                
        
        switch NSParagraphStyle.defaultWritingDirection(forLanguage: dominantLanguage) {
        case .leftToRight:
            if layoutDirection == .rightToLeft {
                return .trailing
            } else {
                return .leading
            }
           
        case .rightToLeft:
            if layoutDirection == .leftToRight {
                return .trailing
            } else {
                return .leading
            }
        case .natural:
            return .leading
            
        @unknown default:
            return .leading
        }
    }


    
    private var naturalTextAlignment: TextAlignment {
        guard let dominantLanguage = dominantLanguage else {
            // If we can't identify the strings language, use the system language's natural alignment
            return .leading
        }
        
        
        switch NSParagraphStyle.defaultWritingDirection(forLanguage: dominantLanguage) {
        case .leftToRight:
            if layoutDirection == .rightToLeft {
                return .trailing
            } else {
                return .leading
            }
           
        case .rightToLeft:
            if layoutDirection == .leftToRight {
                return .trailing
            } else {
                return .leading
            }
        case .natural:
            return .leading
        @unknown default:
            return .leading
        }
    }
    
    private var dominantLanguage: String? {
        let firstChar = "\(text.first ?? " ")"
           return NLLanguageRecognizer.dominantLanguage(for: firstChar)?.rawValue
    }
 
}

الان نرجع لـ ContentView ونبدل الـ Text بـ NaturalText

struct ContentView: View {
    
    var body: some View {

        ScrollView {
        VStack {
        
            NaturalText(text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.")
                .padding()

            
            NaturalText(text: "هذا النص هو مثال لنص يمكن أن يستبدل في نفس المساحة، لقد تم توليد هذا النص من مولد النص العربى، حيث يمكنك أن تولد مثل هذا النص أو العديد من النصوص الأخرى إضافة إلى زيادة عدد الحروف التى يولدها التطبيق.إذا كنت تحتاج إلى عدد أكبر من الفقرات يتيح لك مولد النص العربى زيادة عدد الفقرات كما تريد، النص لن يبدو مقسما ولا يحوي أخطاء لغوية، مولد النص العربى مفيد لمصممي المواقع على وجه الخصوص، حيث يحتاج العميل فى كثير من الأحيان أن يطلع على صورة حقيقية لتصميم الموقع.ومن هنا وجب على المصمم أن يضع نصوصا مؤقتة على التصميم ليظهر للعميل الشكل كاملاً،دور مولد النص العربى أن يوفر على المصمم عناء البحث عن نص بديل لا علاقة له بالموضوع الذى يتحدث عنه التصميم فيظهر بشكل لا يليق. هذا النص يمكن أن يتم تركيبه على أي تصميم دون مشكلة فلن يبدو وكأنه نص منسوخ، غير منظم، غير منسق، أو حتى غير مفهوم. لأنه مازال نصاً بديلاً ومؤقتاً.")
                .padding()

            NaturalText(text: "Try one line of text")
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding()

            NaturalText(text: "تجربة سطر واحد من النص")
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding()

            Spacer()
        }
        }
        
    }

}

هل انتيهنا ؟ لا
هناك امراً اخراً الـ EditorText في حال تطبيقك يدعم الكتابة
مثلاً تطبيق يوميات او تطبيق To Do list بمعنى تطبيق يتطلب من المستخدم كتابة نصوص

راح تواجهه نفس المشكلة انه اتجاه نص الكتابة يعتمد على لغة التطبيق ولا لغة النص !

مثال للمشكلة

تقدر تعمل ملف جديد باسم NaturalTextEditor
وتكرر نفس الكود فقط تحتاج تغير Text الى TextEditor

struct NaturalTextEditor: View {
    @Environment(\.layoutDirection) private var layoutDirection

   @Binding var text: String

    var body: some View {
            TextEditor(text: $text)
                .frame(alignment: naturalAlignment)
                .multilineTextAlignment(naturalTextAlignment)
    }
    
    private var naturalAlignment: Alignment {
        guard let dominantLanguage = dominantLanguage else {
            // If we can't identify the strings language, use the system language's natural alignment
            return .leading
        }
                
        
        switch NSParagraphStyle.defaultWritingDirection(forLanguage: dominantLanguage) {
        case .leftToRight:
            if layoutDirection == .rightToLeft {
                return .trailing
            } else {
                return .leading
            }
           
        case .rightToLeft:
            if layoutDirection == .leftToRight {
                return .trailing
            } else {
                return .leading
            }
        case .natural:
            return .leading
            
        @unknown default:
            return .leading
        }
    }
    
    private var naturalTextAlignment: TextAlignment {
        guard let dominantLanguage = dominantLanguage else {
            // If we can't identify the strings language, use the system language's natural alignment
            return .leading
        }
        
        
        switch NSParagraphStyle.defaultWritingDirection(forLanguage: dominantLanguage) {
        case .leftToRight:
            if layoutDirection == .rightToLeft {
                return .trailing
            } else {
                return .leading
            }
           
        case .rightToLeft:
            if layoutDirection == .leftToRight {
                return .trailing
            } else {
                return .leading
            }
        case .natural:
            return .leading
            
        @unknown default:
            return .leading
        }
    }

     private var dominantLanguage: String? {
        
        let firstChar = "\(text.first ?? " ")"
           return NLLanguageRecognizer.dominantLanguage(for: firstChar)?.rawValue
}
}

الان تستطيع استخدامه بهذا الشكل
اولا تعرف متغير من نوع State

    @State private var text = ""

وتستخدمه بهذا الشكل

 NaturalTextEditor(text: $text)

النتيجة

يتضح في هذا المقال ، اوقات كثيره تحتاج فقط تفكر خارج الصندوق لتجد الحل المناسب =)

جميع مقالاتي من تجاربي وخبرتي الخاصة ، بمعنى لن تجدها في اي موقع او كتاب اخر =)

تستطيع تحميل الكود كامل من صفحتي في Gits