How to translate / localize a variable in Swift - swift

I have this code and I have localization setup successfully. I created a Categories.strings file and localized it, but the translation of the categories is not working.
Code:
struct CategoriesViewRow: View {
var selection: SimpleJson
var body: some View {
HStack {
Image(selection.name)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
Text(String(localized: "\(selection.name)", table: "Categories", comment: "Category"))
Spacer()
}
}
}
This var selection: SimpleJson is a struct that looks like this:
struct SimpleJson: Hashable, Codable, Identifiable {
var id: Int
var name: String
var description: String?
var hidden: Bool?
}
It contains some static categories loaded from a file. They need to be loaded from the file, and I always managed to translate them in Objective-C. But now in Swift, it seems like I am missing something.
Normal SwiftUI Text() code gets translated correctly, but with my other code, there is something wrong.
I am currently using Xcode Beta 13.0, but I am quite sure this isn't the problem. I rather think I am doing something wrong, but I can't seem to find some good info for my use-case.
Can someone help me out?
Thanks in advance

You can add this function to a String extension and then call .localize() on string variables:
extension String {
/**
So that when you return a String it also adjusts the language
- Parameter comment: To describe specific meaning if it's unclear (e.g.: bear could mean the animal or the verb to bear)
- Returns: The localized String duh
- Usage: str.localize()
- Further reading: https://www.hackingwithswift.com/example-code/uikit/how-to-localize-your-ios-app
*/
func localize(comment: String = "") -> String {
NSLocalizedString(self, comment: comment)
}
}

Related

Making struct conform to Identifiable crashes app

EDITS:
removed id: \.self
removed .onDelete from List (mistake in editing)
removed removeRows function
change the Note struct to only hash the id
I am making an app in swiftUI that returns a list of notes. I have a struct, note, that is defined as follows:
struct Note: Identifiable, Hashable {
let id = UUID()
var title: String
var content: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
It conforms to Identifiable so that it can be selected inside a List, and it conforms to Hashable so that it can be held inside a Set (for selection).
My view that (I think) is being super slow and eventually crashing is as follows:
struct SearchView: View {
#State private var searchValue: String = ""
#State private var searchResults: [Note] = [
Note(title: "Hi", content: "whats up"),
Note(title: "wassup", content: "hi")
]
#State private var selectKeeper = Set<Note>()
var body: some View {
VStack(alignment: .leading) {
SearchInputView(searchValue: $searchValue, searchResults: $searchResults)
List(searchResults, selection: $selectKeeper) { note in
Text(note.title )
}
}
}
}
SearchInputView is basically just a view that uses combine to run this function on the contents of a textfield every keypress:
func findIn(notes: [Note], pattern: String) -> [Note] {
if pattern.isEmpty { return notes }
var matchedNotes: [Note] = []
for note in notes {
let note = Note(title: note.title.uppercased(), content: note.content.uppercased())
let pattern = pattern.uppercased()
if note.title.contains(pattern) || note.content.contains(pattern) {
matchedNotes.append(note)
}
}
return matchedNotes
}
This all used to work fine with a ForEach loop and the Note struct not conforming to Identifiable, but right now for whatever reason, the app crashes as soon as I type anything into the text box. I have no idea why this is happening, and I can't see anything in the profilers that might tell me what's going on. The only thing is that as soon as I type all of the uses goes up to 100% and the app crashes. Any ideas?
Thanks!
I figured out that my problem was due to putting the findIn function outside of the struct that was using it. I am not at all sure why this is, but putting it inside of the struct calling it caused it to work out fine. Thanks so much to everyone who provided helpful suggestions in the comments!

SwiftUI Text doesn't localise when inside ForEach

When my Text gets it's string from an array, it doesn't localise, but when I hard-code it in, it does:
Localizable.strings
"greeting1" = "Hello there";
"greeting2" = "Howdy";
(Localised into English)
My View
var localisedStrings = ["greeting1", "greeting2"]
struct Message: View {
var body: some View {
ForEach(localisedStrings, id: \.self) { string in
Text(string)
}
}
}
The text will just write the key, like
"greeting1", rather than "Hello there"
Is there a way to force the Text object to use a localised string? Or is it changing somehow because it's getting it's content from a ForEach loop?
My View (hard coded)
//var localisedStrings = ["greeting1", "greeting2"]
struct Message: View {
var body: some View {
//ForEach(localisedStrings, id: \.self) { string in
Text("greeting1")
//}
}
}
This works just fine, and results in the text displaying "Hello there".
It works when you hardcode the string in, because you are using this initialiser, which takes a LocalizedStringKey.
LocalizedStringKey is ExpressibleByStringLiteral, but the word string in Text(string) is not a "string literal", so it calls this initialiser instead, and is not localised. You can initialise an instance of LocalizedStringKey directly, in order to localise it:
Text(LocalizedStringKey(string))
It's because here you are using plain String Swift type
var localisedStrings = ["greeting1", "greeting2"]
and here you are using SwiftUI own LocalizedStringKey type
Text("greeting1")
To fix your issue map your string to LocalizedStringKey
struct Message: View {
var body: some View {
ForEach(localisedStrings, id: \.self) { string in
Text(LocalizedStringKey(string))
}
}
}
Text.init(_:tableName:bundle:comment:)
Please follow the linked documentation to learn more.

Declare an array of structs whose content conforms to View protocol

Prefix: I'm a newbie regarding Generics in Swift. That's why the following problem and the resulting compiler messages are hard to understand for me.
Working with SwiftUI, I made a struct 'AlignedForm' that should use an Array to hold several 'AlignedFormRow' structs. These AlignedFormRows consist of a text label on the right and an arbitrary view on the left. They are implemented as a generic type like this:
struct AlignedFormRow<Content>: View where Content: View {
let content: Content
let title: String
let titleWidth: CGFloat
init(_ title: String, titleWidth: CGFloat, #ViewBuilder content: #escaping () -> Content) {
self.title = title
self.titleWidth = titleWidth
self.content = content()
}
var body: some View {
HStack {
Text(title).frame(width: titleWidth, alignment: .trailing)
content
}
}
}
Now, I can create content for my form rows in nice SwiftUI-style like this:
VStack {
AlignedFormRow("Full Name", titleWidth:100.0) {
TextField("your name", text: $name)
}
AlignedFormRow("Email", titleWidth:100.0) {
TextField("your email", text: $email)
}
}
Which results in two aligned labelled text fields:
So far it works fine. Now, I would like to create a helper struct "AlignedForm" that manages the titleWidth for all rows. But when I try to implement AlignedForm, I'm running into problems declaring a 'rows' Array:
struct AlignedForm {
let rows: [AlignedFormRow<Content: View>]
}
The code doesn't compile and the compiler responds to that line with "Expected '>' to complete generic argument list" which doesn't make much sense to me. I blindly tried many different ways to declare this rows array, but it never compiles. What's the correct syntax here? Or is this approach flawed otherwise?
I don't know why you wrote AlignedFormRow<Content: View>.
First you need to create a struct that conforms to View protocol, then use it in the declaration. Like,
struct AlignedFormView: View {
var body: some View {
EmptyView()
}
}
Then, use it in your array.
struct AlignedForm {
let rows: [AlignedFormRow<AlignedFormView>]
}
This is the way. Thank you

SwiftUI ForEach Binding compile time error looks like not for-each

I'm starting with SwiftUI and following WWDC videos I'm starting with #State and #Binding between two views. I got a display right, but don't get how to make back-forth read-write what was not include in WWDC videos.
I have model classes:
class Manufacturer {
let name: String
var models: [Model] = []
init(name: String, models: [Model]) {
self.name = name
self.models = models
}
}
class Model: Identifiable {
var name: String = ""
init(name: String) {
self.name = name
}
}
Then I have a drawing code to display that work as expected:
var body: some View {
VStack {
ForEach(manufacturer.models) { model in
Text(model.name).padding()
}
}.padding()
}
and I see this:
Canvas preview picture
But now I want to modify my code to allows editing this models displayed and save it to my model #Binding so I've change view to:
var body: some View {
VStack {
ForEach(self.$manufacturer.models) { item in
Text(item.name)
}
}.padding()
}
But getting and error in ForEach line:
Generic parameter 'ID' could not be inferred
What ID parameter? I'm clueless here... I thought Identifiable acting as identifier here.
My question is then:
I have one view (ContentView) that "holds" my datasource as #State variable. Then I'm passing this as #Binding to my ManufacturerView want to edit this in List with ForEach fill but cannot get for each binding working - how can I do that?
First, I'm assuming you have something like:
#ObservedObject var manufacturer: Manufacturer
otherwise you wouldn't have self.$manufacturer to begin with (which also requires Manufacturer to conform to ObservableObject).
self.$manufacturer.models is a type of Binding<[Model]>, and as such it's not a RandomAccessCollection, like self.manufacturer.models, which is one of the overloads that ForEach.init accepts.
And if you use ForEach(self.manufacturer.models) { item in ... }, then item isn't going to be a binding, which is what you'd need for, say, a TextField.
A way around that is to iterate over indices, and then bind to $manufacturer.models[index].name:
ForEach(manufacturer.indices) { index in
TextField("model name", self.$manufacturer.models[index].name)
}
In addition to that, I'd suggest you make Model (and possibly even Manufacturer) a value-type, since it appears to be just a storage of data:
struct Model: Identifiable {
var id: UUID = .init()
var name: String = ""
}
This isn't going to help with this problem, but it will eliminate possible issues with values not updating, since SwiftUI wouldn't detect a change.

Binding an element of an array of an ObservableObject : 'subscript(_:)' is deprecated

I'm using an ObservableObject 'DataStore', which contains an array ('exampleList') of objects ('exampleObject').
#Published exampleList = [exampleObject]()
I'm calling the DataStore via #EnvironmentObject ('dataStore').
#EnvironmentObject var dataStore = DataStore()
Then I iterate the list with
ForEach(0..<dataStore.exampleList.count) { index in ....
To bind element of item to a detail view, I'm doing like this:
DetailView(itemBinding: $dataStore.exampleList[index])
Until Xcode11 beta 4, it worked perfectly. Since XCode11 beta 5, it still works but Xcode gives me this alert:
'subscript(_:)' is deprecated: See Release Notes for a migration path
I tried with simpler stuff, with a simple #State var containing an array of strings, and it's the same issue: when calling an element of this array, and trying to use the value into a TextField:
TextField("test", text: $test[0])
I get the same alert.
I don't understand how to fix it. Does that mean that we no longer can bind values inside an array?
Then, how can we iterate an array and bind a specific item?
This is my first question on Stack Overflow, I apologize if my question is clumsy...
Thanks a lot for your answers, I'm using Stack Overflow for years, it's amazing, I always find existing and helpful answers, but it is the first time I can't find any, that's why I'm asking.
Xcode 11, beta 6 UPDATE:
Good news! Just as I suspected, in beta 6, the Binding conformance to MutableCollection has been been replaced with something else. Instead of conforming to MutableCollection, it now let your access the elements via #dynamicMemberLookup. The result is you now can keep doing $text[3] and no longer get a warning! It seems this question can be closed now.
Xcode 11, beta 5. Old answer:
I finally got some time to investigate this a little. As I mentioned in the comments, I think it would be wise to wait until the Collection conformance is completely removed (or replaced with something else). But just to satisfy our curiosity, I have created an extension on Binding, that I think does what the current Collection conformance does. The only difference is that, instead of accessing through a subscript, I implemented a function called element(_ idx: Int) to get a Binding<T> to the element.
If one day the conformance is completely removed, I may change the implementation, and conform to Collection myself. I cannot do it now, because it would conflict with the existent (and deprecated) implementation. For the time being, I think this demonstrate how to handle the warnings if you absolutely want to get rid of them.
Just to be clear. I am not using this code. As long as I can still access the elements through the subscript, I will still do it and ignore the warnings. This is just for academic purposes.
The extension is:
extension Binding where Value: MutableCollection, Value.Index == Int {
func element(_ idx: Int) -> Binding<Value.Element> {
return Binding<Value.Element>(
get: {
return self.wrappedValue[idx]
}, set: { (value: Value.Element) -> () in
self.wrappedValue[idx] = value
})
}
}
And it can be used like this:
struct MainView: View {
#Binding var text: [String]
var body: some View {
TextField("", text: $text.element(0))
TextField("", text: $text.element(1))
TextField("", text: $text.element(2))
}
}
I had to bind the array of an observable object recently, didn't get any warnings on stable XCode11. I did it like this
struct ScheduleTimer: Identifiable {
var id: Int
var name: String
var start: Date
var end: Date
var isActive: Bool
}
struct ScheduleView: View {
#ObservedObject var scheduleController = ScheduleController()
var body: some View {
NavigationView {
Form {
ForEach(scheduleController.timers) { timer in
ScheduleForm(scheduleController: self.scheduleController, timer: timer)
}
}
}
}
}
struct ScheduleForm: View {
#ObservedObject var scheduleController: ScheduleController
var timer: ScheduleTimer
var scheduleIndex: Int {
scheduleController.timers.firstIndex(where: { $0.id == timer.id })!
}
#State var start = Date()
var body: some View {
Section(header: Text(self.scheduleController.timers[scheduleIndex].name)){
DatePicker("From", selection: self.$scheduleController.timers[scheduleIndex].start, displayedComponents: .hourAndMinute)
DatePicker("To", selection: self.$scheduleController.timers[scheduleIndex].end, displayedComponents: .hourAndMinute)
Toggle(isOn: self.$scheduleController.timers[scheduleIndex].isActive) {
Text("")
}.toggleStyle(DefaultToggleStyle())
}
}
}
class ScheduleController: ObservableObject {
#Published var timers = [ScheduleTimer]()
...