How to convert Binding<String?> to Binding<String> in SwiftUI - ios

How to convert Binding<String?> to Binding in SwiftUI
variable declared as:
#Binding var name: String?

It depends where do you use it, but most probably, you'd need something, like
Binding(name) ?? .constant("")
In some cases it might be needed proxy binding created in place, like in https://stackoverflow.com/a/59274498/12299030.
For scenarios with arguments you can use approach like in https://stackoverflow.com/a/62217832/12299030.

You can create your own Binding Extension with default values for cases in where Value is Optional.
Swift 5 Extension
extension Binding where Value: Equatable {
/// Given a binding to an optional value, creates a non-optional binding that projects
/// the unwrapped value. If the given optional binding contains `nil`, then the supplied
/// value is assigned to it before the projected binding is generated.
///
/// This allows for one-line use of optional bindings, which is very useful for CoreData types
/// which are non-optional in the model schema but which are still declared nullable and may
/// be nil at runtime before an initial value has been set.
///
/// class Thing: NSManagedObject {
/// #NSManaged var name: String?
/// }
/// struct MyView: View {
/// #State var thing = Thing(name: "Bob")
/// var body: some View {
/// TextField("Name", text: .bind($thing.name, ""))
/// }
/// }
///
/// - note: From experimentation, it seems that a binding created from an `#State` variable
/// is not immediately 'writable'. There is seemingly some work done by SwiftUI following the render pass
/// to make newly-created or assigned bindings modifiable, so simply assigning to
/// `source.wrappedValue` inside `init` is not likely to have any effect. The implementation
/// has been designed to work around this (we don't assume that we can unsafely-unwrap even after
/// assigning a non-`nil` value), but a side-effect is that if the binding is never written to outside of
/// the getter, then there is no guarantee that the underlying value will become non-`nil`.
#available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
static func bindOptional(_ source: Binding<Value?>, _ defaultValue: Value) -> Binding<Value> {
self.init(get: {
source.wrappedValue ?? defaultValue
}, set: {
source.wrappedValue = ($0 as? String) == "" ? nil : $0
})
}
}
Usage
struct ContentView: View { 
#Binding var title: String?
var body: some View {
TextField("Title Value", text: .bindOptional($title, "Default Title"))
}
}

Related

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 }
}

SwiftUI Localisation not working with #State Strings

I have implemented Localisation in my SwiftUI app. Everything works fine but I'm having issues with localising #State var. Localisation is not working and I'm getting only the keys printed out. Any idea how to fix this issue?
The value of type is already in my Localizable.strings
#State var type: String
var body: some View {
VStack {
Text(self.type) // not working
Text("test") // working
}
}
You can take convert the string into a NSLocalizedString
Text(NSLocalizedString(type, comment: ""))
or change the type of type into a LocalizedStringKey
#State var type: LocalizedStringKey
When a string literal is passed to Text its type needs to be inferred (Since it isn't explicitly stated). Literal text is probably a fixed part of your UI, so it is interpreted as a LocalizedStringKey.
When you pass the property self.type, it has an explicit type - String, so the Text(_ verbatim:) initialiser is used resulting in non-localised text.
If you want that property to be localised you can use the LocalizedStringKey(_ string: String) initialiser:
Text(LocalizedStringKey(self.type))

How to use Combine framework NSObject.KeyValueObservingPublisher?

I'm trying to use the Combine framework NSObject.KeyValueObservingPublisher. I can see how to produce this publisher by calling publisher(for:options:) on an NSObject. But I'm having two problems:
I can include .old in the options, but no .old value ever arrives. The only values that appear are the .initial value (when we subscribe) and the .new value (each time the observed property changes). I can suppress the .initial value but I can't suppress the .new value or add the .old value.
If the options are [.initial, .new] (the default), I see no way to distinguish whether the value I'm receiving is .initial or .new. With "real" KVO I get an NSKeyValueChangeKey or an NSKeyValueObservedChange that tells me what I'm getting. But with the Combine publisher, I don't. I just get unmarked values.
It seems to me that these limitations make this publisher all but unusable except in the very simplest cases. Are there any workarounds?
I don't have much to add to TylerTheCompiler's answer, but I want to note a few things:
NSObject.KeyValueObservingPublisher doesn't use the change dictionary internally. It always uses the key path to get the value of the property.
If you pass .prior, the publisher will publish both the before and the after values, separately, each time the property changes. This is due to how KVO is implemented by Objective-C. It's not specific to KeyValueObservingPublisher.
A shorter way to get the before and after values of the property is by using the scan operator:
extension Publisher {
func withPriorValue() -> AnyPublisher<(prior: Output?, new: Output), Failure> {
return self
.scan((prior: Output?.none, new: Output?.none)) { (prior: $0.new, new: $1) }
.map { (prior: $0.0, new: $0.1!) }
.eraseToAnyPublisher()
}
}
If you also use .initial, then the first output of withPriorValue will be be (prior: nil, new: currentValue).
For getting the old value, the only workaround I was able to find was to use .prior instead of .old, which causes the publisher to emit the current value of the property before it is changed, and then combine that value with the next emission (which is the new value of the property) using collect(2).
For determining what's an initial value vs. a new value, the only workaround I found was to use first() on the publisher.
I then merged these two publishers and wrapped it all up in a nice little function that spits out a custom KeyValueObservation enum that lets you easily determine whether it's an initial value or not, and also gives you the old value if it's not an initial value.
Full example code is below. Just create a brand new single-view project in Xcode and replace the contents of ViewController.swift with everything below:
import UIKit
import Combine
/// The type of value published from a publisher created from
/// `NSObject.keyValueObservationPublisher(for:)`. Represents either an
/// initial KVO observation or a non-initial KVO observation.
enum KeyValueObservation<T> {
case initial(T)
case notInitial(old: T, new: T)
/// Sets self to `.initial` if there is exactly one element in the array.
/// Sets self to `.notInitial` if there are two or more elements in the array.
/// Otherwise, the initializer fails.
///
/// - Parameter values: An array of values to initialize with.
init?(_ values: [T]) {
if values.count == 1, let value = values.first {
self = .initial(value)
} else if let old = values.first, let new = values.last {
self = .notInitial(old: old, new: new)
} else {
return nil
}
}
}
extension NSObjectProtocol where Self: NSObject {
/// Publishes `KeyValueObservation` values when the value identified
/// by a KVO-compliant keypath changes.
///
/// - Parameter keyPath: The keypath of the property to publish.
/// - Returns: A publisher that emits `KeyValueObservation` elements each
/// time the property’s value changes.
func keyValueObservationPublisher<Value>(for keyPath: KeyPath<Self, Value>)
-> AnyPublisher<KeyValueObservation<Value>, Never> {
// Gets a built-in KVO publisher for the property at `keyPath`.
//
// We specify all the options here so that we get the most information
// from the observation as possible.
//
// We especially need `.prior`, which makes it so the publisher fires
// the previous value right before any new value is set to the property.
//
// `.old` doesn't seem to make any difference, but I'm including it
// here anyway for no particular reason.
let kvoPublisher = publisher(for: keyPath,
options: [.initial, .new, .old, .prior])
// Makes a publisher for just the initial value of the property.
//
// Since we specified `.initial` above, the first published value will
// always be the initial value, so we use `first()`.
//
// We then map this value to a `KeyValueObservation`, which in this case
// is `KeyValueObservation.initial` (see the initializer of
// `KeyValueObservation` for why).
let publisherOfInitialValue = kvoPublisher
.first()
.compactMap { KeyValueObservation([$0]) }
// Makes a publisher for every non-initial value of the property.
//
// Since we specified `.initial` above, the first published value will
// always be the initial value, so we ignore that value using
// `dropFirst()`.
//
// Then, after the first value is ignored, we wait to collect two values
// so that we have an "old" and a "new" value for our
// `KeyValueObservation`. This works because we specified `.prior` above,
// which causes the publisher to emit the value of the property
// _right before_ it is set to a new value. This value becomes our "old"
// value, and the next value emitted becomes the "new" value.
// The `collect(2)` function puts the old and new values into an array,
// with the old value being the first value and the new value being the
// second value.
//
// We then map this array to a `KeyValueObservation`, which in this case
// is `KeyValueObservation.notInitial` (see the initializer of
// `KeyValueObservation` for why).
let publisherOfTheRestOfTheValues = kvoPublisher
.dropFirst()
.collect(2)
.compactMap { KeyValueObservation($0) }
// Finally, merge the two publishers we created above
// and erase to `AnyPublisher`.
return publisherOfInitialValue
.merge(with: publisherOfTheRestOfTheValues)
.eraseToAnyPublisher()
}
}
class ViewController: UIViewController {
/// The property we want to observe using our KVO publisher.
///
/// Note that we need to make this visible to Objective-C with `#objc` and
/// to make it work with KVO using `dynamic`, which means the type of this
/// property must be representable in Objective-C. This one works because it's
/// a `String`, which has an Objective-C counterpart, `NSString *`.
#objc dynamic private var myProperty: String?
/// The thing we have to hold on to to cancel any further publications of any
/// changes to the above property when using something like `sink`, as shown
/// below in `viewDidLoad`.
private var cancelToken: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
// Before this call to `sink` even finishes, the closure is executed with
// a value of `KeyValueObservation.initial`.
// This prints: `Initial value of myProperty: nil` to the console.
cancelToken = keyValueObservationPublisher(for: \.myProperty).sink {
switch $0 {
case .initial(let value):
print("Initial value of myProperty: \(value?.quoted ?? "nil")")
case .notInitial(let oldValue, let newValue):
let oldString = oldValue?.quoted ?? "nil"
let newString = newValue?.quoted ?? "nil"
print("myProperty did change from \(oldString) to \(newString)")
}
}
// This prints:
// `myProperty did change from nil to "First value"`
myProperty = "First value"
// This prints:
// `myProperty did change from "First value" to "Second value"`
myProperty = "Second value"
// This prints:
// `myProperty did change from "Second value" to "Third value"`
myProperty = "Third value"
// This prints:
// `myProperty did change from "Third value" to nil`
myProperty = nil
}
}
extension String {
/// Ignore this. This is just used to make the example output above prettier.
var quoted: String { "\"\(self)\"" }
}

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

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>?.

Resources