UPDATE: I updated the code to my previous semi-working solution because there were multiple answers, but none answered the question the way I need it to work.
Also, note that I need United States at the top of the picker, even if it appears again in the alphabetical country listing.
I am trying to create a picker that displays a country name, and depending on what country is selected, stores the corresponding country id. This way the user sees the name of the country but I can pass only the country id into my database.
The code I have so far shows the list of country names, and stores that country name in the selectedCountry variable. It also updates the text element in the HStack properly.
The only thing that is not working is storing the corresponding countryId.
I am using SwiftUI with the latest Swift 5 and XCode 13.1.
Here's what I've got so far:
import SwiftUI
struct Country: View {
#State private var selectedCountry = ""
#State private var selectedCountryId = ""
let countryId = Locale.isoRegionCodes
let countryArray = Locale.isoRegionCodes.compactMap { Locale.current.localizedString(forRegionCode: $0) }
var body: some view {
HStack {
Text("Country:")
.font(.system(size: 17))
Spacer()
Text("")
if selectedCountry != "" {
Text("\(selectedCountry)")
.font(.system(size: 17))
.foregroundColor(Color("WhiteText"))
} else {
Text("Select Country")
.font(.system(size: 17))
.foregroundColor(Color("GrayText"))
}
} // End HStack
.onTapGesture {
self.showsCountryPicker.toggle()
}
Picker("Country", selection: $selectedCountry) {
ForEach(countryArray, id: \.self) {
Text($0)
}
}
.pickerStyle(WheelPickerStyle())
.padding()
.labelsHidden()
}
}
}
I'm sure it's completely the wrong way to do this, so don't worry so much about correcting my code. I'd really just love to know how to do this, because I'll also need to implement the same thing when it comes to selecting a US State (i.e. show the full name of the State but store the abbreviation).
Also, there is much more to the body view, but I've stripped down the code here just to show this specific issue.
Thanks in advance!
The Picker documentation says to use the tag modifier on each Text to control what the Picker stores in its selection.
There's no reason to store an array of country names if you just want to store the selected country code. And you should use SwiftUI's Environment to get the current Locale, so that your view will be redrawn if the user changes her locale.
import SwiftUI
import PlaygroundSupport
struct CountryPicker: View {
#Binding var countryId: String
#Environment(\.locale) var locale
var body: some View {
Picker("", selection: $countryId) {
ForEach(Locale.isoRegionCodes, id: \.self) { iso in
Text(locale.localizedString(forRegionCode: iso)!)
.tag(iso)
}
}
}
}
struct Test: View {
#State var countryId: String = ""
var body: some View {
VStack {
CountryPicker(countryId: $countryId)
Text("You picked \(countryId).")
}
.padding()
}
}
PlaygroundPage.current.setLiveView(Test())
I appreciate all the assistance, but I got it all working the way I needed it to. For this I am storing only the Country ID, which is all I need, but translating that ID into the country name for the text element in the HStack.
Here's the answer:
import SwiftUI
// Struct to store the country name and ID
fileprivate struct Country {
var id: String
var name: String
}
// Function to put United States at the top of the list
fileprivate func getLocales() -> [Country] {
let locales = Locale.isoRegionCodes
.filter { $0 != "United States"}
.compactMap { Country(id: $0, name: Locale.current.localizedString(forRegionCode: $0) ?? $0)}
return [Country(id: "US", name: Locale.current.localizedString(forRegionCode: "US") ?? "United States")] + locales
}
struct Test: view {
// selectedCountry stores the countryID (i.e. US)
#State private var selectedCountry: String = ""
// Main UI
var body: some View {
HStack {
Text("Country:")
.font(.system(size: 17))
Spacer()
Text("")
if selectedCountry != "" {
Text(Locale.current.localizedString(forRegionCode: selectedCountry) ?? selectedCountry)
.font(.system(size: 17))
.foregroundColor(Color("WhiteText"))
} else {
Text("Select Country")
.font(.system(size: 17))
.foregroundColor(Color("GrayText"))
}
} // End HStack
Picker("Country", selection: $selectedCountry) {
ForEach(getLocales(), id: \.id) { country in
Text(country.name).tag(country.id)
}
}
}
}
I would just do something simple like this:
struct Country: Identifiable, Hashable {
let id: String
let name: String
}
struct CountryView: View {
let countries = Locale.isoRegionCodes.compactMap{
Country(id: $0, name: Locale.current.localizedString(forRegionCode: $0)!) }
#State var selectedCountry: Country?
var body: some View {
Picker("Country", selection: $selectedCountry) {
ForEach(countries) {
Text($0.name).tag(Optional($0))
}
}.pickerStyle(.wheel)
.onChange(of: selectedCountry) { selected in
if let cntry = selected {
print("--> store country id: \(cntry.id)")
}
}
}
}
If you want to sort the countries, use this:
ForEach(countries.sorted(by: { $0.name < $1.name })) { ... }
This my friend is where dictionaries come in handy. A dictionary has two parts Key and Value or in swift terms ["Key":"Value"] There are three things to note about a dictionary.
#1, all key-value-pairs MUST be the same type, for example, [32:"String", 33: "String"] Which is important to remember.
#2, it does NOT guarantee order.
#3, It can only contain unique keys.
How does this apply to your problem? Well, it has to do with the type of data that you have. Currently you have 2 separate arrays, one with the Country, and one with the Codes. You can combine them into the dictionary and ensure that they are always together, referencing the value from the key, or searching for the value, to get a key or multiple keys. That previous sentence is important to pay attention to, though not for your case, you're guaranteed to only get one value for one key, but you can get multiple keys from one value. In your case you have a unique country, and unique ID.
var countries = ["USA": 9999,
"UK": 9998,
"Canada": 9997] // Etc..
Getting a value from a dictionary is even easier, but works similar to an array. You sub-script it. For example:
var canadaID= countries["Canada"]
Now it gets trickier getting a key from a value because you have to iterate over the whole dictionary to grab it. It's also possible that there are duplicate values, meaning you could technically get back an array of "Keys". In this example, I grabbed only the first found value. Again, remember that the order is not guaranteed and if you have multiple of the same value you may get the incorrect key.
var countryID = 9998
if let key = countries.first(where: { $0.value == someValue })?.key {
print(key)
}
From here it becomes trivial to store it.
func storeCountryIDFromKey(country: String) {
let countryId = countries[country]
// Store your ID.
}
What if my order is important??!??
This could be important for your case as you might want to display the countries in alphabetical order. To do that simply map the keys to an array and sort, as is tradition.
let keys: [String] = countries.map{String($0.key) }
Solution
This is a working solution. I'll leave it up to you to sort the arrays and link the data where you need it to go. You could use onChange(...) or even a Button(..) to handle the update, however your ID is the selectedCountry in this example.
struct FirstView: View {
#State var countries = ["US": 1,
"UK": 2,
"Canada": 4]
#State var selectedCountry = 1
var body: some View {
VStack {
Picker("Country", selection: $selectedCountry) {
let countriesArray = countries.keys.map({$0})
ForEach(countriesArray, id: \.self ) { country in
Text(country).tag(countries[country]!)
}
}.pickerStyle(.wheel)
Text("\(selectedCountry)")
}
}
}
Additional Reading
There is a concept in programming called Big-O notation typically expressed as O(n) or pronounced O-of-N. Which is the way that we describe time and space complexities. It's a great skill to learn if you want to become a great developer as it has to do with Data Structures and Algorithms. To make more sense of this, as it applies to your question, having two separate arrays to loop over vs one dictionary effectively takes 2x as long to accomplish with the double arrays. Furthermore it doubles the space complexity. Combining both into one Dictionary reduces your performance overhead by 1/2 which is a huge performance gain. With a small data-set such as countries, which there are a finite amount, it doesn't really matter; However, if you start working with massive datasets then suddenly 1/2 faster is a substantial performance boost.
Without digging too much into it, and to simply get your wheels spinning, every time you make a variable, or the compiler does that for you, that increases space complexity. Every time you run a line of code, or loop over a line of code, that increases the time complexity. Always, and I mean always, try your best to reduce that overhead. It'll force you to think outside the box and in turn, you'll learn better practices.
For creating and sorting the array of countries, this is my suggestion
// create a Country struct
struct Country:Equatable{
let code:String
let name:String
}
/** creating the array by first getting the
the codes and then sorting it, bubbling US to the top
[Sorted by][1]
**/
let countries = Locale.isoRegionCodes.compactMap
{ Country(code:$0,name:Locale.current.localizedString(forRegionCode: $0) ?? "")
}.sorted{
switch ($0,$1){
case ($0,$1) where $0.code == "US":
return true
case ($0,$1) where $1.code == "US":
return false
default:
return $0.name<$1.name
}
}
You can now store your selection and get the code and name or whatever you wish to by changing the struct as per your needs
I'm working on an app that needs to open on the users last used view even if the app is completly killed by the user or ios.
As a result I'm holding last view used in UserDefaults and automatically moving the user through each view in the stack until they reach their destination.
The code on each view is as follows:
#Binding var redirectionID: Int
VStack() {
List {
NavigationLink(destination: testView(data: data, moc: moc), tag: data.id, selection:
$redirectionId) {
DataRow(data: data)
}
}
}.onAppear() {
redirectionID = userData.lastActiveView
}
Is there a better / standard way to achieve this? This works reasonably on iOS 14.* but doesn't work very well on iOS 13.* On iOS 13.* The redirection regularly doesnt reach its destination page and non of the preceeding views in the stack seem to be created. Pressing back etc results in a crash.
Any help / advice would be greatly appreciated.
This sounds like the perfect use of if SceneStorage
"You use SceneStorage when you need automatic state restoration of the value. SceneStorage works very similar to State, except its initial value is restored by the system if it was previously saved, and the value is· shared with other SceneStorage variables in the same scene."
#SceneStorage("ContentView.selectedProduct") private var selectedProduct: String?
#SceneStorage("DetailView.selectedTab") private var selectedTab = Tabs.detail
It is only available in iOS 14+ though so something manual would have to be implemented. Maybe something in CoreData. An object that would have variables for each important state variable. It would work like an ObservedObject ViewModel with persistence.
Also. you can try...
"An NSUserActivity object captures the app’s state at the current moment in time. For example, include information about the data the app is currently displaying. The system saves the provided object and returns it to the app the next time it launches. The sample creates a new NSUserActivity object when the user closes the app or the app enters the background."
Here is some sample code that summarizes how to bring it all together. It isn't a minimum reproducible example because it is a part of the larger project called "Restoring Your App's State with SwiftUI" from Apple. But it gives a pretty good picture on how to implement it.
struct ContentView: View {
// The data model for storing all the products.
#EnvironmentObject var productsModel: ProductsModel
// Used for detecting when this scene is backgrounded and isn't currently visible.
#Environment(\.scenePhase) private var scenePhase
// The currently selected product, if any.
#SceneStorage("ContentView.selectedProduct") private var selectedProduct: String?
let columns = Array(repeating: GridItem(.adaptive(minimum: 94, maximum: 120)), count: 3)
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(productsModel.products) { product in
NavigationLink(destination: DetailView(product: product, selectedProductID: $selectedProduct),
tag: product.id.uuidString,
selection: $selectedProduct) {
StackItemView(itemName: product.name, imageName: product.imageName)
}
.padding(8)
.buttonStyle(PlainButtonStyle())
.onDrag {
/** Register the product user activity as part of the drag provider which
will create a new scene when dropped to the left or right of the iPad screen.
*/
let userActivity = NSUserActivity(activityType: DetailView.productUserActivityType)
let localizedString = NSLocalizedString("DroppedProductTitle", comment: "Activity title with product name")
userActivity.title = String(format: localizedString, product.name)
userActivity.targetContentIdentifier = product.id.uuidString
try? userActivity.setTypedPayload(product)
return NSItemProvider(object: userActivity)
}
}
}
.padding()
}
.navigationTitle("ProductsTitle")
}
.navigationViewStyle(StackNavigationViewStyle())
.onContinueUserActivity(DetailView.productUserActivityType) { userActivity in
if let product = try? userActivity.typedPayload(Product.self) {
selectedProduct = product.id.uuidString
}
}
.onChange(of: scenePhase) { newScenePhase in
if newScenePhase == .background {
// Make sure to save any unsaved changes to the products model.
productsModel.save()
}
}
}
}
I am creating a little workout app using SwiftUI. I have a list of exercises stored in Core Data, and when the user selects one from the list I am adding it to an array in the state.
#State private var workoutExercises: [CDWorkoutExercise] = []
...
func onSelectExercise(exercise: CDExercise) {
let newWorkoutExercise = CDWorkoutExercise(context: self.moc)
newWorkoutExercise.exercise = exercise
newWorkoutExercise.reps = 8
newWorkoutExercise.sets = 3
workoutExercises.append(newWorkoutExercise)
}
I have a ForEach that loops over the exercise core data objects that have been added to the array and display the exercise they added as well as allow the user to use an input, preferably a Stepper or Textfield, to change the number of reps to perform for the exercise.
ForEach(workoutExercises, id: \.self) { workoutExercise in
VStack {
Text(workoutExercise.exercise?.wrappedName ?? "Unknown")
Stepper("Reps", value: $workoutExercise.reps, in: 1...100)
}
}
However, when I try to bind the object to the input Xcode displays the error Use of unresolved identifier: $workoutExercise (in this case on the line where the Stepper is defined) and I'm unsure how to resolve the issue or where I've gone wrong.
If I correctly understood your intention and your model the following should work
TextField("", text: Binding<String>(
get: {workoutExercise.exercise?.wrappedName ?? "Unknown"},
set: {workoutExercise.exercise?.wrappedName = $0}))
I want to call my ImageView with some kind of modifier to make the Image in it round or square. The code should look like:
ImageView(withURL: newsItem.imageUrl).test(Circle())
To get this behaviour i read you can extend the view like i did below. It works perfectly fine with a String but i don't understand what's different with a Shape?
I get the error Message Protocol type 'Shape' cannot conform to 'Shape' because only concrete types can conform to protocols but i don't understand that either :/. Can someone explain me the Problem and what i can do about it?
struct ImageView: View {
var clipShape: Shape
var body: some View {
ZStack {
Image("swift")
.resizable()
.aspectRatio(contentMode: .fill)
.clipShape(self.clipShape)
.shadow(radius: 6)
}
}
}
extension ImageView {
func test(_ shape: Shape) -> Self {
var copy = self
copy.clipShape = shape
return copy
}
}
btw this Code is shortened to only show the Code for the specific Problem, the View contains more than that. Just so you don't question the use of it.
Thanks in adnvance!
EDIT:
For why shapes don't work and what to do look at: https://forums.swift.org/t/how-to-make-different-clipshape/27448/2
In my Case if found out simply clipShaping the whole ImageView does the job just fine.
I also wanna link this Creating custom modifiers for Swift UI views where you can read about custom modifiers
I found this question while looking for the same title, but different intention - I want to create a modifier that can be applies specifically to something conforming to Shape so that I can do Shape like effects with it - stroke, etc.
But while I haven't found my answer yet, I think the answer to yours is that ViewModifier doesn't work to insert/change internal values of a view - instead you should be considering a ViewBuilder.
There aren't many articles that I've found really highlighting how to and when to effectively choose to use ViewBuilder, but this one from Majid entitled The power of #ViewBuilder in SwiftUI does a pretty good job.
Apple's docs on ViewBuilder also (fortunately) include a bit of sample code in the reference (one of the few places for SwiftUI) that at least show you some basic ideas of how you can use it.
The basic gist is that ViewModifier always works in general with methods on View, where ViewBuilder is used as a means of composing views together, which sounds much more like what you want. In the end, they overlap significantly in what they can do - differing primarily in how they achieve it.
Its a 3 step process:
Make a ViewModifier
Extend View with a function to apply your modifier and return the modified view
Call your modifying function on an actual View
struct RedBackGroundModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(Color.red)
}
}
extension View {
func makeTheBackGroundRed() -> some View {
self.modifier(RedBackGroundModifier())
}
}
struct ContentView: View {
var body: some View {
Text("Hello, World!").makeTheBackGroundRed()
}
}
I am trying to write unit tests for SwiftUI views but finding zero resources on the web for how to go about that.
I have a view like the following
struct Page: View {
#EnvironmentObject var service: Service
var body: some View {
NavigationView {
ScrollView(.vertical) {
VStack {
Text("Some text"))
.font(.body)
.navigationBarTitle(Text("Title")))
Spacer(minLength: 100)
}
}
}
}
}
I started writing a test like this
func testPage() {
let page = Page().environmentObject(Service())
let body = page.body
XCTAssertNotNil(body, "Did not find body")
}
But then how do I get the views inside the body? How do I test their properties? Any help is appreciated.
Update:
As a matter of fact even this doesn't work. I am getting the following runtime exception
Thread 1: Fatal error: body() should not be called on ModifiedContent<Page,_EnvironmentKeyWritingModifier<Optional<Service>>>.
There is a framework created specifically for the purpose of runtime inspection and unit testing of SwiftUI views: ViewInspector
You can extract your custom views to verify the inner state, trigger UI-input side effects, read the formatted text values, assure the right text styling is applied, and much more:
// Side effect from tapping on a button
try sut.inspect().find(button: "Close").tap()
let viewModel = try sut.inspect().view(CustomView.self).actualView().viewModel
XCTAssertFalse(viewModel.isDialogPresented)
// Testing localization + formatting
let sut = Text("Completed by \(72.51, specifier: "%.1f")%")
let string = try sut.inspect().text().string(locale: Locale(identifier: "es"))
XCTAssertEqual(string, "Completado por 72,5%")
The test for your view could look like this:
func testPage() throws {
let page = Page().environmentObject(Service())
let string = try page.inspect().navigationView().scrollView().vStack().text(0).string()
XCTAssertEqual(string, "Some text")
}
Update: Let's all try using the ViewInspector library by nalexn!
Original reply:
Until Apple
a) designs testability into SwiftUI, and
b) exposes this testability to us,
we're screwed, and will have to use UI Testing in place of unit testing… in a complete inversion of the Testing Pyramid.