'Bool' is not convertible to 'Binding<Bool>' in SwiftUI - swiftui

I have a SwiftUI native Watch app I Am working on. I have a Combine based class that allows me to store `\userDefaults, one of which is a simple toggle.
import SwiftUI
import Foundation
import Combine
class MeetingSetup: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var twitterEnabled: Bool = false {
didSet {
willChange.send()
}
}
init() {
let prefs:UserDefaults = UserDefaults(suiteName: "group.com.appname")!
twitterEnabled = prefs.bool(forKey: "keyTwitterEnabledBool")
}
}
In the SwiftUI I am getting the error messages that Bool is not convertible to Binding<Bool>
import SwiftUI
import Combine
struct SetupView : View {
#ObjectBinding var meetingSetup: MeetingSetup = delegate.meetingSetup
var body: some View {
HStack{
Toggle(isOn: self.meetingSetup.twitterEnabled){ // <== 'Bool' in not convertible to 'Binding<Bool>'
Text("Twitter")
}
}
}
I don't understand why this is getting the message since the code is #ObjectBinding, should it not be Binding<Bool> by definition? If not how do I address this correctly??

You missed the dollar sign:
Toggle(isOn: self.$meetingSetup.twitterEnabled) { ... }
I also noticed that you are using didSetin your #BindableObject, but you should really be using willSet.
And finally, maybe you pasted incompletely, but you are missing a closing bracket in your view.
If you don't know what the dollar sign is for, check the WWDC2019 video Data Flow in SwiftUI.

Related

Is it possible to reload only modified item in SwiftUI List/ForEach that uses #Binding

Simple sample code with toggle button (slightly modified from hackingwithswift:
This code(hackingwithswift original and my version) IS redrawing every list cell whenever any toggle happens. I modified code to better debug view drawing.
import SwiftUI
struct User: Identifiable {
let id = UUID()
var name: String
var isContacted = false
}
struct ProfileView: View {
#State private var users = [
User(name: "Taylor"),
User(name: "Justin"),
User(name: "Adele")
]
var body: some View {
let _ = Self._printChanges()
List($users) { $user in
ProfileCell(user: $user)
}
}
}
struct ProfileCell: View{
#Binding var user: User
var body: some View{
let _ = Self._printChanges()
Text(user.name)
Spacer()
Toggle("User has been contacted", isOn: $user.isContacted)
.labelsHidden()
}
}
Running app and toggling will print following in console for every toggle:
ProfileView: _users changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
ProfileCell: #self, _user changed.
Hackingwithswift tutorial states "Using a binding in this way is the most efficient way of modifying the list, because it won’t cause the entire view to reload when only a single item changes.", however that does not seem to be true.
Is it possible to redraw only item that was changed?
Theoretically it should be working, but it seems they changed something since first introduction, because now on state change they recreate(!) bindings (all of them), so automatic view changes handler interpret that as view update (binding is a property after all).
A possible workaround for this is to help rendering engine and check view equitability manually.
Tested with Xcode 13.4 / iOS 15.5
Main parts:
// 1
List($users) { $user in
EquatableView(content: ProfileCell(user: $user)) // << here !!
}
// 2
struct ProfileCell: View, Equatable {
static func == (lhs: ProfileCell, rhs: ProfileCell) -> Bool {
lhs.user == rhs.user
}
// ...
// 3
struct User: Identifiable, Equatable {
Test module is here

Equatable object not updated when content updated

I'm trying to wrap my head around how SwiftUI deals with Equatable, and I can't understand why onChange doesn't seem to trigger when an equatable object is updated.
Below is an example of what I'm confused about. If you use the slider, the text updates just fine, but the OnChange doesn't trigger. It only triggers if you change the number of objects in the Array.
PointAnnotation is equatable by default, but to double-check it's comparing latitude I've also made it adhere to equatable manually. Doing so doesn't change the behavior.
What am I missing?
import SwiftUI
struct ContentView: View {
#ObservedObject var controller = Controller()
#State private var change:Bool = false
var body: some View {
VStack {
Text("Obj 0 lat: \(controller.object.annotations[0].coordinate.latitude)")
.padding()
Text("Obj changed: \(change ? "true" : "false")")
Slider(value: $controller.object.annotations[0].coordinate.latitude, in: (0...10))
}
.onChange(of: controller.object, perform: { value in change = true })
}
}
class Controller:ObservableObject {
#Published var object = Object()
}
struct Object:Equatable {
var annotations:[MapAnnotation] = MapAnnotation.exampleArray
}
import MapKit
class MapAnnotation:MKPointAnnotation {
var id:String = UUID().uuidString
}
extension MapAnnotation {
static var exampleArray:[MapAnnotation] = [ MapAnnotation(), MapAnnotation(), MapAnnotation() ]
}

crashing playground with ForEach

I have this code, see below, it contains a ForEach loop which is commented out. the same view in an App runs just fine even if the ForEach loop is enabled. However, in a playground it crashes with a very unhelpful error message:
"error: Playground execution aborted: error: Execution was interrupted, reason: signal SIGABRT.
The process has been left at the point where it was interrupted, use "thread return -x" to return to the state before expression evaluation."
I tried finding information about this message. From what I understand it means that lldb does not know exactly what goes wrong and prints this. So, I turn to stack overflow in the hope someone does know what exactly might by going wrong here....?
import Cocoa
import SwiftUI
import PlaygroundSupport
import Combine
struct RowModel : Identifiable, Hashable {
var text : String
var id : UUID = UUID()
}
class My : ObservableObject {
#Published var s: String = "Hi there"
#Published var elements = [
RowModel(text: "een"),
RowModel(text: "twee"),
RowModel(text: "drie"),
RowModel(text: "vier"),
]
}
struct Row : View {
var item : String
var body : some View {
Text(item)
}
}
struct Screen : View {
#StateObject var my = My()
var body: some View {
VStack {
Text("The screen")
VStack {
Row(item: my.elements[0].text)
Row(item: my.elements[1].text)
// ForEach(0 ..< my.elements.count, id: \.self){ (index : Int) in
//
// Row(item: my.elements[index].text)
// }
}.frame(height: 100)
TextField("enter values", text: $my.s)
}
}
}
var view = Screen()
PlaygroundPage.current.setLiveView(view)
I'm late to the party, but this bug is still not fixed at this point so I figured I'd leave this for any future viewer.
This seems to be an issue with results bar on the right, because moving the view to a separate file in Sources (press Command 0) works fine.
there is a workaround for this bug
move ContentView to a separate file in Sources directory
make your models public
more info
https://stackoverflow.com/a/67120969/2027018

How to load a webpage in an ovally inside a SwiftUI App vs loading in safari

The functionality I'm looking to create is what the ESPN app does when you click on one of its alerts... it loads the app but instead of formatting the view it loads a Safari view over the app that can be tapped away (honestly I hate it in that instance but in ones like these it would work great.)
current code for reference
Button(action: {
openURL(URL(string: "linkhere")!)
}) {
Image("LISTENMENU")
}
Am I going to need to setup another view and build the webkitview myself or can this functionality be specified another way? (perhaps by tinkering with the openURL string
You need to wrap the SFSafariViewController (which is from UIKit) into SwiftUI, since it isn't possible to do this natively right now. You should use UIViewControllerRepresentable to do this.
import SwiftUI
import SafariServices
struct MyView: View {
#State var showStackoverflow:Bool = false
var body: some View {
Button(action: { self.showStackoverflow = true }) {
Text("Open stackoverflow")
}
.sheet(isPresented: self.$showStackoverflow) {
SFSafariViewWrapper(url: URL(string: "https://stackoverflow.com")!)
}
}
}
struct SFSafariViewWrapper: UIViewControllerRepresentable {
let url: URL
func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> SFSafariViewController {
return SFSafariViewController(url: url)
}
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<SFSafariViewWrapper>) {
return
}
}

SwiftUI: How to persist #Published variable using UserDefaults?

I want a #Published variable to be persisted, so that it's the same every time when I relaunch my app.
I want to use both the #UserDefault and #Published property wrappers on one variable. For example I need a '#PublishedUserDefault var isLogedIn'.
I have the following propertyWrapper
import Foundation
#propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
This is my Settings class
import SwiftUI
import Combine
class Settings: ObservableObject {
#Published var isLogedIn : Bool = false
func doLogin(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
self.login = myresponse.login
}
}
}
}
My View class
struct HomeView : View {
#EnvironmentObject var settings: Settings
var body: some View {
VStack {
if settings.isLogedIn {
Text("Loged in")
} else{
Text("Not Loged in")
}
}
}
}
Is there a way to make a single property wrapper that covers both the persisting and the publishing?
import SwiftUI
import Combine
fileprivate var cancellables = [String : AnyCancellable] ()
public extension Published {
init(wrappedValue defaultValue: Value, key: String) {
let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
self.init(initialValue: value)
cancellables[key] = projectedValue.sink { val in
UserDefaults.standard.set(val, forKey: key)
}
}
}
class Settings: ObservableObject {
#Published(key: "isLogedIn") var isLogedIn = false
...
}
Sample: https://youtu.be/TXdAg_YvBNE
Version for all Codable types check out here
To persist your data you could use the #AppStorage property wrapper.
However, without using #Published your ObservableObject will no longer put out the news about the changed data. To fix this, simply call objectWillChange.send() from the property's willSet observer.
import SwiftUI
class Settings: ObservableObject {
#AppStorage("Example") var example: Bool = false {
willSet {
// Call objectWillChange manually since #AppStorage is not published
objectWillChange.send()
}
}
}
It should be possible to compose a new property wrapper:
Composition was left out of the first revision of this proposal,
because one can manually compose property wrapper types. For example,
the composition #A #B could be implemented as an AB wrapper:
#propertyWrapper
struct AB<Value> {
private var storage: A<B<Value>>
var wrappedValue: Value {
get { storage.wrappedValue.wrappedValue }
set { storage.wrappedValue.wrappedValue = newValue }
}
}
The main benefit of this approach is its predictability: the author of
AB decides how to best achieve the composition of A and B, names it
appropriately, and provides the right API and documentation of its
semantics. On the other hand, having to manually write out each of the
compositions is a lot of boilerplate, particularly for a feature whose
main selling point is the elimination of boilerplate. It is also
unfortunate to have to invent names for each composition---when I try
the compose A and B via #A #B, how do I know to go look for the
manually-composed property wrapper type AB? Or maybe that should be
BA?
Ref: Property WrappersProposal: SE-0258
You currently can't wrap #UserDefault around #Published since that is not currently allowed.
The way to implement #PublishedUserDefault is to pass an objectWillChange into the wrapper and call it before setting the variable.
struct HomeView : View {
#StateObject var auth = Auth()
#AppStorage("username") var username: String = "Anonymous"
var body: some View {
VStack {
if username != "Anonymous" {
Text("Logged in")
} else{
Text("Not Logged in")
}
}
.onAppear(){
auth.login()
}
}
}
import SwiftUI
import Combine
class Auth: ObservableObject {
func login(params:[String:String]) {
Webservice().login(params: params) { response in
if let myresponse = response {
UserDefaults.standard.set(myresponse.login, forKey: "username")`
}
}
}
}