I know, this is one of those "Not working in iOS XX" questions, but I'm completely stuck...
So I have an ObservableObject class that inherits from NSObject, because I need to listen to the delegate methods of UISearchResultsUpdating.
class SearchBarListener: NSObject, UISearchResultsUpdating, ObservableObject {
#Published var searchText: String = ""
let searchController: UISearchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.searchResultsUpdater = self
}
func updateSearchResults(for searchController: UISearchController) {
/// Publish search bar text changes
if let searchBarText = searchController.searchBar.text {
print("text: \(searchBarText)")
self.searchText = searchBarText
}
}
}
struct ContentView: View {
#ObservedObject var searchBar = SearchBarListener()
var body: some View {
Text("Search text: \(searchBar.searchText)")
.padding()
/// more code that's not related
}
}
The problem is that even though print("text: \(searchBarText)") prints fine, the Text("Search text: \(searchBar.searchText)") is never updated (in iOS 13). It works fine in iOS 14.
Here's a minimal reproducible example:
class SearchBarTester: NSObject, ObservableObject {
#Published var searchText: String = ""
override init() {
super.init()
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
print("updated")
self.searchText = "Updated"
}
}
}
struct ContentView: View {
#ObservedObject var searchBar = SearchBarTester()
var body: some View {
NavigationView {
Text("Search text: \(searchBar.searchText)")
.padding()
}
}
}
After 5 seconds, "updated" is printed in the console, but the Text doesn't change. In iOS 14, the Text changes to "Search text: Updated" as expected.
However, if I don't inherit from NSObject, both iOS 13 and iOS 14 work!
class SearchBarTester: ObservableObject {
#Published var searchText: String = ""
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
print("updated")
self.searchText = "Updated"
}
}
}
I think the problem has something to do with inheriting from a class. Maybe it's something that was fixed in iOS 14. But does anyone know what is going on?
Edit
Thanks #Cuneyt for the answer! Here's the code that finally worked:
import SwiftUI
import Combine /// make sure to import this
class SearchBarTester: NSObject, ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var searchText: String = "" {
willSet {
self.objectWillChange.send()
}
}
override init() {
super.init()
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
print("updated")
self.searchText = "Updated"
}
}
}
It appears to be on iOS 13, if you subclass an object and do not conform to ObservableObject directly (as in class SearchBarTester: ObservableObject), you'll need to add this boilerplate code:
#Published var searchText: String = "" {
willSet {
objectWillChange.send()
}
}
However, calling the default objectWillChange will still not work, hence you'll need to define it yourself again:
let objectWillChange = PassthroughSubject<Void, Never>()
Related
After implementing a search function for my application I want the search results to be clickable.
Therefore, I embedded the search result into a Navigation Link, but something is wrong with that.
When I try to search for an object, the search result gets greyed out and the Navigation Link is not clickable.
Normally it looks like this (I know, that everyone knows that)
I implemented the search result in the the code snipped below under NavigationSearch()
To reproduce this error:
Use Mark van Wijnen's NavigationSearch out of his Medium article
This class:
import SwiftUI
struct TracksView2: View {
var tracks: [Track] = tracksData
#State private var searchText: String = ""
var filtered : [Track] {
if searchText.isEmpty {
return tracks
} else {
return tracks.filter({ $0.search(searchText) })
}
}
var body: some View {
NavigationView {
NavigationSearch(text: $searchText, searchResultsContent: {
ForEach(filtered, id: \.id) { track in
NavigationLink(destination: TrackDetailView(tracks: track)) {
TrackRowView(track: track)
}
}
})
} //: NAVIGATION
}
}
(And replace TrackDetailView with an EmptyView)
Create a datamodel like this:
import SwiftUI
struct Track: Identifiable {
var id = UUID()
var title: String
var headline: String
var image: String
var imagebig: String
var gradientColors: [Color]
var link: String
var description: String
var details: [String]
func search(_ query: String) -> Bool {
let searchable = [title, headline, description] + details
return searchable.filter({ $0.contains(query) }).count > 0
}
}
And use this sample data:
import SwiftUI
let tracksData: [Track] = [
Track(
title: "Spa-Francorchamps",
headline: "Die Ardennenachterbahn ist für viele die faszinierenste Rennstrecke überhaupt. ",
image: "road",
imagebig: "preview",
gradientColors: [Color("ColorBlueberryLight"), Color("ColorBlueberryDark")],
link: "https://www.wikipedia.de/",
description: """
Bis
""",
details: ["7004m","8,50-18,50m", "21","100m"]
),
Thanks in advance :)
Here is the UISearchBar wrapped in a UIViewRepresentable, credit to Tim W Swift UI Contacts. I have used it and it works. Maybe give it a try?
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String?
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.autocapitalizationType = .none // --> here, we make some adjustments to the view so it better fits to our app
searchBar.searchBarStyle = .minimal
searchBar.placeholder = placeholder
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
I have this kind of code in a SwiftUI app:
#Published var myFlag = false {
willSet {
... useful code ...
objectWillChange.send()
}
}
It more or less works, but to exactly get what I need I want to have something like that:
#Published var myFlag = false {
didSet {
... useful code ...
objectDidChange.send()
}
}
For the simple reason that I would like to be notified once myFlag is updated rather than before. It doesn't seem to work that way though.
Is there some other way to get what I want?
Beside I am using this code:
.onReceive(self.appState.$myFlag) {...}
inside ContentView.swift
#Published automatically publishes when the property is at willSet
https://developer.apple.com/documentation/combine/published
If you prefer to publish during didSet remove #Published and publish manually.
import SwiftUI
class ManualOObject: ObservableObject {
var someValue: Int = 0{
willSet{
print("willSet - \(someValue)")
}
didSet{
print("didSet - \(someValue)")
objectWillChange.send()
}
}
init() {
for n in 0...5 {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(n)) {
print("n = \(n.description)")
self.someValue = n
}
}
}
}
struct ManualOOView: View {
#StateObject var manualOObject = ManualOObject()
var body: some View {
Text(manualOObject.someValue.description).onReceive(manualOObject.objectWillChange, perform: { _ in
print("UI received = \(manualOObject.someValue)")
})
}
}
I have a problem with observed object in SwiftUI.
I can see changing values of observed object on the View struct.
However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.
here's my code sample (this is my ObservableObject)
class settingCodeTwo: ObservableObject {
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
and the main problem is... "self.codeTwo.text" never changed!
class NetworkManager: ObservableObject {
#ObservedObject var codeTwo = settingCodeTwo()
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
and this is view, I can catch change of the value in this one
import SwiftUI
import Combine
struct SettingView: View {
#ObservedObject var codeTwo = settingCodeTwo()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
Text(codeTwo.text)
}
}
}
}
Help me please.
Non-SwiftUI Code
Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
Use a subscriber like Sink to observe changes to any publisher. (Every #Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.
Reason for SwiftUI View not reacting to class property changes:
struct is a value type so when any of it's properties change then the value of the struct has changed
class is a reference type, when any of it's properties change, the underlying class instance is still the same.
If you assign a new class instance then you will notice that the view reacts to the change.
Approach:
Use a separate view and that accepts codeTwoText as #Binding that way when the codeTwoText changes the view would update to reflect the new value.
You can keep the model as a class so no changes there.
Example
class Model : ObservableObject {
#Published var name : String //Ensure the property is `Published`.
init(name: String) {
self.name = name
}
}
struct NameView : View {
#Binding var name : String
var body: some View {
return Text(name)
}
}
struct ContentView: View {
#ObservedObject var model : Model
var body: some View {
VStack {
Text("Hello, World!")
NameView(name: $model.name) //Passing the Binding to name
}
}
}
Testing
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let model = Model(name: "aaa")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
model.name = "bbb"
}
return ContentView(model: model)
}
}
It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
#objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no #ObservedObject needed here
...
I'm experimenting with SwiftUI and would like to fetch an update from my REST API with a search string.
However, I'm not sure how to bring the two components together now.
I hope you have an idea.
Here my Code:
struct ContentView: View {
#State private var searchTerm: String = ""
#ObservedObject var gameData: GameListViewModel = GameListViewModel(searchString: ### SEARCH STRING ???? ###)
var body: some View {
NavigationView{
Group{
// Games werden geladen...
if(self.gameData.isLoading) {
LoadingView()
}
// Games sind geladen:
else{
VStack{
// Suche:
searchBarView(text: self.$searchTerm)
// Ergebnisse:
List(self.gameData.games){ game in
NavigationLink(destination: GameDetailView(gameName: game.name ?? "0", gameId: 0)){
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(game.name ?? "Kein Name gefunden")
.font(.headline)
Text("Cover: \(game.cover?.toString() ?? "0")")
.font(.subheadline)
.foregroundColor(.gray)
}
}
}
}
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
And the search bar implementation:
import Foundation
import SwiftUI
struct searchBarView: UIViewRepresentable {
#Binding var text:String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>){
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
print(searchText)
text = searchText
}
}
func makeCoordinator() -> searchBarView.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<searchBarView>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<searchBarView>) {
uiView.text = text
}
}
The search text should be inside the view model.
final class GameListViewModel: ObservableObject {
#Published var isLoading: Bool = false
#Published var games: [Game] = []
var searchTerm: String = ""
private let searchTappedSubject = PassthroughSubject<Void, Error>()
private var disposeBag = Set<AnyCancellable>()
init() {
searchTappedSubject
.flatMap {
self.requestGames(searchTerm: self.searchTerm)
.handleEvents(receiveSubscription: { _ in
DispatchQueue.main.async {
self.isLoading = true
}
},
receiveCompletion: { comp in
DispatchQueue.main.async {
self.isLoading = false
}
})
.eraseToAnyPublisher()
}
.replaceError(with: [])
.receive(on: DispatchQueue.main)
.assign(to: \.games, on: self)
.store(in: &disposeBag)
}
func onSearchTapped() {
searchTappedSubject.send(())
}
private func requestGames(searchTerm: String) -> AnyPublisher<[Game], Error> {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
return Fail(error: URLError(.badURL))
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.mapError { $0 as Error }
.decode(type: [Game].self, decoder: JSONDecoder())
.map { searchTerm.isEmpty ? $0 : $0.filter { $0.title.contains(searchTerm) } }
.eraseToAnyPublisher()
}
}
Each time onSearchTapped is called, it fires a request for new games.
There's plenty of things going on here - let's start from requestGames.
I'm using JSONPlaceholder free API to fetch some data and show it in the List.
requestGames performs the network request, decodes [Game] from the received Data. In addition to that, the returned array is filtered using the search string (because of the free API limitation - in a real world scenario you'd use a query parameter in the request URL).
Now let's have a look at the view model constructor.
The order of the events is:
Get the "search tapped" subject.
Perform a network request (flatMap)
Inside the flatMap, loading logic is handled (dispatched on the main queue as isLoading uses a Publisher underneath, and there will be a warning if a value is published on a background thread).
replaceError changes the error type of the publisher to Never, which is a requirement for the assign operator.
receiveOn is necessary as we're probably still in a background queue, thanks to the network request - we want to publish the results on the main queue.
assign updates the array games on the view model.
store saves the Cancellable in the disposeBag
Here's the view code (without the loading, for the sake of the demo):
struct ContentView: View {
#ObservedObject var viewModel = GameListViewModel()
var body: some View {
NavigationView {
Group {
VStack {
SearchBar(text: $viewModel.searchTerm,
onSearchButtonClicked: viewModel.onSearchTapped)
List(viewModel.games, id: \.title) { game in
Text(verbatim: game.title)
}
}
}
.navigationBarTitle(Text("Games"))
}
}
}
Search bar implementation:
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var onSearchButtonClicked: (() -> Void)? = nil
class Coordinator: NSObject, UISearchBarDelegate {
let control: SearchBar
init(_ control: SearchBar) {
self.control = control
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
control.text = searchText
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
control.onSearchButtonClicked?()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
There is no need to get UIKit involved, you can declare a simple search bar like this:
struct SearchBar: View {
#State var searchString: String = ""
var body: some View {
HStack {
TextField(
"Start typing",
text: $searchString,
onCommit: performSearch)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: performSearch) {
Image(systemName: "magnifyingglass")
}
} .padding()
}
func performSearch() {
}
}
and then place the search logic inside performSearch().
I have a class conforming to the #ObservableObject protocol and created a subclass from it with it's own variable with the #Published property wrapper to manage state.
It seems that the #published property wrapper is ignored when using a subclass. Does anyone know if this is expected behaviour and if there is a workaround?
I'm running iOS 13 Beta 8 and xCode Beta 6.
Here is an example of what I'm seeing. When updating the TextField on MyTestObject the Text view is properly updated with the aString value. If I update the MyInheritedObjectTextField the anotherString value isn't updated in the Text view.
import SwiftUI
class MyTestObject: ObservableObject {
#Published var aString: String = ""
}
class MyInheritedObject: MyTestObject {
#Published var anotherString: String = ""
}
struct TestObserverWithSheet: View {
#ObservedObject var myTestObject = MyInheritedObject()
#ObservedObject var myInheritedObject = MyInheritedObject()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("Update aString", text: self.$myTestObject.aString)
Text("Value of aString is: \(self.myTestObject.aString)")
TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
}
}
}
}
Finally figured out a solution/workaround to this issue. If you remove the property wrapper from the subclass, and call the baseclass objectWillChange.send() on the variable the state is updated properly.
NOTE: Do not redeclare let objectWillChange = PassthroughSubject<Void, Never>() on the subclass as that will again cause the state not to update properly.
I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.
Here is a fully working example:
import SwiftUI
class MyTestObject: ObservableObject {
#Published var aString: String = ""
}
class MyInheritedObject: MyTestObject {
// Using #Published doesn't work on a subclass
// #Published var anotherString: String = ""
// If you add the following to the subclass updating the state also doesn't work properly
// let objectWillChange = PassthroughSubject<Void, Never>()
// But if you update the value you want to maintain state
// of using the objectWillChange.send() method provided by the
// baseclass the state gets updated properly... Jaayy!
var anotherString: String = "" {
willSet { self.objectWillChange.send() }
}
}
struct MyTestView: View {
#ObservedObject var myTestObject = MyTestObject()
#ObservedObject var myInheritedObject = MyInheritedObject()
var body: some View {
NavigationView {
VStack(alignment: .leading) {
TextField("Update aString", text: self.$myTestObject.aString)
Text("Value of aString is: \(self.myTestObject.aString)")
TextField("Update anotherString", text: self.$myInheritedObject.anotherString)
Text("Value of anotherString is: \(self.myInheritedObject.anotherString)")
}
}
}
}
iOS 14.5 resolves this issue.
Combine
Resolved Issues
Using Published in a subclass of a type conforming to ObservableObject now correctly publishes changes. (71816443)
This is because ObservableObject is a protocol, so your subclass must conform to the protocol, not your parent class
Example:
class MyTestObject {
#Published var aString: String = ""
}
final class MyInheritedObject: MyTestObject, ObservableObject {
#Published var anotherString: String = ""
}
Now, #Published properties for both class and subclass will trigger view events
UPDATE
This has been fixed in iOS 14.5 and macOS 11.3, subclasses of ObservableObject will correctly publish changes on these versions. But note that the same app will exhibit the original issues when run by a user on any older minor OS version. You still need the workaround below for any class that is used on these versions.
The best solution to this problem that I've found is as follows:
Declare a BaseObservableObject with an objectWillChange publisher:
open class BaseObservableObject: ObservableObject {
public let objectWillChange = ObservableObjectPublisher()
}
Then, to trigger objectWillChange in your subclass, you must handle changes to both observable classes and value types:
class MyState: BaseObservableObject {
var classVar = SomeObservableClass()
var typeVar: Bool = false {
willSet { objectWillChange.send() }
}
var someOtherTypeVar: String = "no observation for this"
var cancellables = Set<AnyCancellable>()
init() {
classVar.objectWillChange // manual observation necessary
.sink(receiveValue: { [weak self] _ in
self?.objectWillChange.send()
})
.store(in: &cancellables)
}
}
And then you can keep on subclassing and add observation where needed:
class SubState: MyState {
var subVar: Bool = false {
willSet { objectWillChange.send() }
}
}
You can skip inheriting BaseObservableObject in the root parent class if that class already contains #Published variables, as the publisher is then synthesized. But be careful, if you remove the final #Published value from the root parent class, all the objectWillChange.send() in the subclasses will silently stop working.
It is very unfortunate to have to go through these steps, because it is very easy to forget to add observation once you add a variable in the future. Hopefully we will get a better official fix.
This happens also when your class is not directly subclass ObservableObject:
class YourModel: NSObject, ObservableObject {
#Published var value = false {
willSet {
self.objectWillChange.send()
}
}
}
From my experience, just chain the subclass objectWillChange with the base class's objectWillChange like this:
class GenericViewModel: ObservableObject {
}
class ViewModel: GenericViewModel {
#Published var ...
private var cancellableSet = Set<AnyCancellable>()
override init() {
super.init()
objectWillChange
.sink { super.objectWillChange.send() }
.store(in: &cancellableSet)
}
}