How can I unwrap an optional value inside a binding in Swift? - ios

I'm building an app using SwiftUI and would like a way to convert a Binding<Value?> to a Binding<Value>.
In my app I have an AvatarView which knows how to render an image for a particular user.
struct AvatarView: View {
#Binding var userData: UserData
...
}
My app holds a ContentView that owns two bindings: a dictionary of users by id, and the id of the user whose avatar we should be showing.
struct ContentView: View {
#State var userById: Dictionary<Int, UserData>
#State var activeUserId: Int
var body: some View {
AvatarView(userData: $userById[activeUserId])
}
}
Problem: the above code doesn't combine because $userById[activeUserId] is of type Binding<UserData?> and AvatarView takes in a Binding<UserData>.
Things I tried...
$userById[activeUserId]! doesn't work because it's trying to unwrap a Binding<UserData?>. You can only unwrap an Optional, not a Binding<Optional>.
$(userById[activeUserId]!) doesn't work for reasons that I don't yet understand, but I think something about $ is resolved at compile time so you can't seem to prefix arbitrary expressions with $.

You can use this initialiser, which seems to handle this exact case - converting Binding<T?> to Binding<T>?:
var body: some View {
AvatarView(userData: Binding($userById[activeUserId])!)
}
I have used ! to force unwrap, just like in your attempts, but you could unwrap the nil however you want. The expression Binding($userById[activeUserId]) is of type Binding<UserData>?.

Related

Updating a #State var in SwiftUI from an async method doesn't work

I have the following View:
struct ContentView: View {
#State var data = [SomeClass]()
var body: some View {
List(data, id: \.self) { item in
Text(item.someText)
}
}
func fetchDataSync() {
Task.detached {
await fetchData()
}
}
#MainActor
func fetchData() async {
let data = await SomeService.getAll()
self.data = data
print(data.first?.someProperty)
// > Optional(115)
print(self.data.first?.someProperty)
// > Optional(101)
}
}
now the method fetchDataSync is a delegate that gets called in a sync context whenever there is new data. I've noticed that the views don't change so I've added the printouts. You can see the printed values, which differ. How is this possible? I'm in a MainActor, and I even tried detaching the task. Didn't help. Is this a bug?
It should be mentioned that the objects returned by getAll are created inside that method and not given to any other part of the code. Since they are class objects, the value might be changed from elsewhere, but if so both references should still be the same and not produce different output.
My theory is that for some reason the state just stays unchanged. Am I doing something wrong?
Okay, wow, luckily I ran into the Duplicate keys of type SomeClass were found in a Dictionary crash. That lead me to realize that SwiftUI is doing some fancy diffing stuff, and using the == operator of my class.
The operator wasn't used for actual equality in my code, but rather for just comparing a single field that I used in a NavigationStack. Lesson learned. Don't ever implement == if it doesn't signify true equality or you might run into really odd bugs later.

How to use optional #State variable with binding parameter in SwiftUI

I am trying to use an optional #State variable with a TextField. This is my code:
struct StorageView: View {
#State private var storedValue: String?
var body: some View {
VStack {
TextField("Type a value", text: $storedValue)
}
}
}
When I use this code, I get the following error:
Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding<String>'
I have tried the following code for both the values to be optional, but nothing seems to work:
TextField("Type a value", text: $storedValue ?? "")
TextField("Type a value", text: Binding($storedValue)!)
How would I go about using an optional #State variable with a binding? Any help with this is appreciated.
How would I go about using an optional #State variable with a binding? Any help with this is appreciated.
It looks like you are using an optional state variable with a binding. You get an error because TextField's initializer expects a Binding<String> rather than a Binding<String?>. I guess you could solve the problem by adding another initializer that accepts Binding<String?>, or maybe making an adapter that converts between Binding<String?> and Binding<String>, but a better option might be to have a good think about why you need your state variable to be optional in the first place. After all this string something that will be displayed in your UI -- what do you expect your TextField to display if storedValue is nil?

SwiftUI: .onOpenURL action closure is not updating #State property which is of type URL

I am implementing my first iOS Application with SwiftUI, in which I want users to be able of clicking on an invitation link for joining a topic group (DeepLinking).
Like joining a WhatsApp-Group with a link.
Therefore I associated my Domain (lets say: https://invite.example.com/) with my Swift-Project.
Whenever I click/open a URL (e.g. https://invite.example.com/313bceff-58e7-40ae-a1bd-b67be466ef72) my app opens and if the user is logged in an the .onOpenURL action method is triggered as expected.
However, if I try to save the url in a #State URL property in the called closure, it gets not stored.
The #State boolean property for showing the sheet is set to true though.
That is my code in the #main struct.
import SwiftUI
#main
struct MyApp: App {
#StateObject private var appRouter: AppRouter = AppRouter()
#State private var openAcceptInvitationSheet: Bool = false
#State private var invitationURL: URL? = nil
var body: some Scene {
WindowGroup {
switch appRouter.currentScreen {
case .launch:
EmptyView()
case .login:
LoginSignupNavigationView()
case let .home(authenticatedUser):
HomeTabView()
.environmentObject(authenticatedUser)
.onOpenURL { url in
invitationURL = url //Gets not set -> url is not nil here!
openAcceptInvitationSheet = true //Is working and sheet will be presented
}
.sheet(isPresented: $openAcceptInvitationSheet) {
//invitationURL is nil -> Why?
AcceptInvitationNavigationView(invitationURL: invitationURL!)
}
}
}
}
}
Everything else is working here as expected. I guess I have a misconception of how the #State properties work. However in all my other views I managed assigning values to #State properties in closures which later can be used.
Rather than using two variables for your sheet, use one – the optional URL.
.sheet(item: $invitationURL) { url in
AcceptInvitationNavigationView(invitationURL: url)
}
The optionality of your URL? state variable takes the place of the boolean value in determining whether the sheet should display, and the sheet receives the unwrapped URL value.
I don't think that your URL is not being set – it's more a question of it's not set at the time the original sheet's closure is evaluated, which is a subtly different SwiftUI object life cycle thing! Sticking to a single object massively simplifies everything. You'll also be able to change your code in AcceptInvitationNavigationView to expect a URL rather than having to deal with being passed an optional URL.
As noted in comments, this only works if URL conforms to Identifiable, which it doesn't by default. But you can use a URL's hashValue to synthesize a unique identifier:
extension URL: Identifiable {
var id: Int { hashValue }
}

NSKeyValueCoding.setValue(_:forKey:) not working in SwiftUI

I am unable to produce a proper minimal working example, mainly due to my novice level understanding of iOS development, but I do have a simple SwiftUI project that may help.
In my ContentView.swift:
import SwiftUI
struct ContentView: View {
#State var viewText :String
var myClass :MyClass
var body: some View {
VStack{
Text(viewText)
.padding()
Button("Update Text", action: {
myClass.update()
viewText = myClass.txt
})
}
}
}
class MyClass: NSObject {
var txt :String = ""
var useSetVal :Bool = false
func update(){
if(useSetVal){
setValue("used set val", forKey: "txt")
} else {
txt = "used ="
}
useSetVal = !useSetVal
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let mc = MyClass()
ContentView(viewText: "", myClass: mc)
}
}
and in my PracticeApp.swift
import SwiftUI
#main
struct PracticeApp: App {
var body: some Scene {
WindowGroup {
let mc = MyClass()
ContentView(viewText: "", myClass: mc)
}
}
}
In this app, I expect to see the text toggle between "used =" and "used setVal" as I push the button. Instead, I get an exception when I call setValue:
Thread 1: "[<Practice.MyClass 0x60000259dc20> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key txt."
I've been reviewing the answers here but since most answers refer to xib and storyboard files (which I either don't have, or don't know how to find), I don't see how they relate.
By the way, even though the app I'm actually trying to fix doesn't use SwiftUI and the issue with setValue is different, it's still true that I either don't have .xib or .storyboard files or I just don't know where to find them.
I'd appreciate help from any one who could either help me figure out the issue with my example, or who can get me closer to solving the issue with my actual app (including how to produce a proper MWE).
I believe what I've already written is sufficient for the issue (at least for a start), but for those interested, I thought I'd add the full story.
The Full Story
I'm new to iOS development, and I've just taken ownership of an old iOS app. It hasn't really been touched since 2017. I noticed an animation that is not working. Though I cannot verify that it ever did work, I have good reason to assume that it once did, but I can't say when it stopped working.
One issue I noticed is that animated properties are supposed to be updated with the NSKeyValueCoding.setValue(_:forKey:) function, but nothing seems to happen when the function is called.
I was able to work around the issue by overriding the setValue function with my own which basically uses a switch statement to map each key to its corresponding value. However, this did not fix the animation or explain why the setValue function isn't working.
Because both the setValue function and the CABasicAnimation.add(_:forKey:) rely on the same keyPath, I wonder if solving one issue might help me solve the other. I've decided to focus on the setValue issue (at least for now).
When I went to work starting a new project to use as an MWE, I noticed that neither the Storyboard nor the SwiftUI interface options provided by Xcode 13.0 (13A233) started me out with a project structure that matched my existing project. It was clear to me that SwiftUI was new and very different from my existing project, but the Storyboard interface wasn't familiar either and after several minutes a reading tutorials, I failed to build a storyboard app that would respond to button presses at all (all the storyboard app tutorials I found seemed to be set up for older versions of Xcode).
SwiftUI will require that you use #ObservedObject to react to changes in an object. You can make this compliant with both observedobject and key-value manipulation as follows:
struct ContentView: View {
#State var viewText :String
#ObservedObject var myClass :MyClass
var body: some View {
VStack{
Text(viewText)
.padding()
Button("Update Text", action: {
myClass.update()
viewText = myClass.txt
})
}
}
}
class MyClass: NSObject, ObservableObject {
#objc dynamic var txt: String = ""
#Published var useSetVal: Bool = false
func update(){
if(useSetVal){
setValue("used set val", forKey: "txt")
} else {
txt = "used ="
}
useSetVal = !useSetVal
}
}
You need to make the txt property available to Objective-C, in order to make it work with KVO/KVC. This is required as the Key-Value Observing/Coding mechanism is an Objective-C feature.
So, either
class MyClass: NSObject {
#objc var txt: String = ""
, or
#objcMembers class MyClass: NSObject {
var txt: String = ""
This will fix the error, and should make your app behave as expected. However, as others have said, you need to make more changes to the code in order to adhere to the SwiftUI paradigms.

Handle Auth state in WindowGroup

I'm new to SwiftUI still and don't really know how to handle best the auth state. If a user is logged in for example i want to redirect him to home screen if not to a certain screen.
I have a service that will tell me if the user is authenticated like: self.authService.isAuthenticated but in my App in WindowGroup i cannot use my service since this is all a struct and i get Cannot use mutating getter on immutable value: 'self' is immutable
I would appreciate a little snippet that can help me solve this here.
My code:
#main
struct MyApp: App, HasDependencies {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Services
private lazy var authService: AuthService = dependencies.authService()
var body: some Scene {
WindowGroup {
if !self.authService.isAuthenticated {
WelcomeView()
} else {
MainView()
}
}
}
}
I suppose you want to handle it just for this time, but i'm proposing you look deeper in SwiftUI bindings and state handlings.
So here we just save the value in a variable in the init since this is getting loaded first.
#main
struct MainApp: App, HasDependencies {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
// MARK: Services
private lazy var authService: AuthService = dependencies.authService()
var isAuth: Bool = false
init() {
isAuth = self.authService.isAuthenticated
}
var body: some Scene {
WindowGroup {
if isAuth {
MainView()
} else {
WelcomeView()
}
}
}
}
The problem is
private lazy var authService: AuthService = dependencies.authService()
(A) SwiftUI rebuilds views in response to, for example, a #StateObject's ObjectWillChangePublisher. Changes an unwatched variable fall silently in a forest without participating in this UI framework, but would be read if you trigger a state change by some other object. Also, I'd guess that service will be rebuilt every time the struct is first built, but I haven't had a use case for this scenario yet, so I don't know.
(B) You've got a mutating variable holding a reference type stored in a value type. As above, store your service as an #StateObject, which is one way SwiftUI gets around this problem of lifetime management.
To get "lazy" loading, call .onAppear { service.load() }.
That said, you have a services / factory container you probably already want to be an #StateObject and injected into the environment. If you store an ObservableObject inside an ObservableObject, the View will react to the outer object only. That object does not link its ObjectWillChangePublisher to inner objects. You will need to either:
(a) individually inject select services into the environment for children to observe
(b) pass those into observable view models that use Combine to subscribe to specific states
(c) use .onReceive and .onChange APIs on Views to link to specific state changes
(C) Conditionals evaluated in App can cause objects stored in that struct to be rebuilt. Good practice is to keep App super clean, like always. Move any conditional logic to a "Root" View for that Scene.

Resources