I have a form where the user enters their address. While they can always enter it manually, I also wanted to provide them with an easy solution with auto complete so that they could just start typing their address and then tap on the correct one from the list and have it auto populate the various fields.
I started by working off of jnpdx's Swift5 solution - https://stackoverflow.com/a/67131376/11053343
However, there are two issues that I cannot seem to solve:
I need the results to be limited to the United States only (not just the continental US, but the entire United States including Alaska, Hawaii, and Puerto Rico). I am aware of how MKCoordinateRegion works with the center point and then the zoom spread, but it doesn't seem to work on the results of the address search.
The return of the results provides only a title and subtitle, where I need to actually extract all the individual address information and populate my variables (i.e. address, city, state, zip, and zip ext). If the user has an apt or suite number, they would then fill that in themselves. My thought was to create a function that would run when the button is tapped, so that the variables are assigned based off of the user's selection, but I have no idea how to extract the various information required. Apple's docs are terrible as usual and I haven't found any tutorials explaining how to do this.
This is for the latest SwiftUI and XCode (ios15+).
I created a dummy form for testing. Here's what I have:
import SwiftUI
import Combine
import MapKit
class MapSearch : NSObject, ObservableObject {
#Published var locationResults : [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables : Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise : ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.region = MKCoordinateRegion()
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ (currentSearchTerm) in
self.searchTermToResults(searchTerm: currentSearchTerm)
})
.sink(receiveCompletion: { (completion) in
//handle error
}, receiveValue: { (results) in
self.locationResults = results
})
.store(in: &cancellables)
}
func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { promise in
self.searchCompleter.queryFragment = searchTerm
self.currentPromise = promise
}
}
}
extension MapSearch : MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//currentPromise?(.failure(error))
}
}
struct MapKit_Interface: View {
#StateObject private var mapSearch = MapSearch()
#State private var address = ""
#State private var addrNum = ""
#State private var city = ""
#State private var state = ""
#State private var zip = ""
#State private var zipExt = ""
var body: some View {
List {
Section {
TextField("Search", text: $mapSearch.searchTerm)
ForEach(mapSearch.locationResults, id: \.self) { location in
Button {
// Function code goes here
} label: {
VStack(alignment: .leading) {
Text(location.title)
.foregroundColor(Color.white)
Text(location.subtitle)
.font(.system(.caption))
.foregroundColor(Color.white)
}
} // End Label
} // End ForEach
} // End Section
Section {
TextField("Address", text: $address)
TextField("Apt/Suite", text: $addrNum)
TextField("City", text: $city)
TextField("State", text: $state)
TextField("Zip", text: $zip)
TextField("Zip-Ext", text: $zipExt)
} // End Section
} // End List
} // End var Body
} // End Struct
Since no one has responded, I, and my friend Tolstoy, spent a lot of time figuring out the solution and I thought I would post it for anyone else who might be interested. Tolstoy wrote a version for the Mac, while I wrote the iOS version shown here.
Seeing as how Google is charging for usage of their API and Apple is not, this solution gives you address auto-complete for forms. Bear in mind it won't always be perfect because we are beholden to Apple and their maps. Likewise, you have to turn the address into coordinates, which you then turn into a placemark, which means there will be some addresses that may change when tapped from the completion list. Odds are this won't be an issue for 99.9% of users, but thought I would mention it.
At the time of this writing, I am using XCode 13.2.1 and SwiftUI for iOS 15.
I organized it with two Swift files. One to hold the class/struct (AddrStruct.swift) and the other which is the actual view in the app.
AddrStruct.swift
import SwiftUI
import Combine
import MapKit
import CoreLocation
class MapSearch : NSObject, ObservableObject {
#Published var locationResults : [MKLocalSearchCompletion] = []
#Published var searchTerm = ""
private var cancellables : Set<AnyCancellable> = []
private var searchCompleter = MKLocalSearchCompleter()
private var currentPromise : ((Result<[MKLocalSearchCompletion], Error>) -> Void)?
override init() {
super.init()
searchCompleter.delegate = self
searchCompleter.resultTypes = MKLocalSearchCompleter.ResultType([.address])
$searchTerm
.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap({ (currentSearchTerm) in
self.searchTermToResults(searchTerm: currentSearchTerm)
})
.sink(receiveCompletion: { (completion) in
//handle error
}, receiveValue: { (results) in
self.locationResults = results.filter { $0.subtitle.contains("United States") } // This parses the subtitle to show only results that have United States as the country. You could change this text to be Germany or Brazil and only show results from those countries.
})
.store(in: &cancellables)
}
func searchTermToResults(searchTerm: String) -> Future<[MKLocalSearchCompletion], Error> {
Future { promise in
self.searchCompleter.queryFragment = searchTerm
self.currentPromise = promise
}
}
}
extension MapSearch : MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
currentPromise?(.success(completer.results))
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//could deal with the error here, but beware that it will finish the Combine publisher stream
//currentPromise?(.failure(error))
}
}
struct ReversedGeoLocation {
let streetNumber: String // eg. 1
let streetName: String // eg. Infinite Loop
let city: String // eg. Cupertino
let state: String // eg. CA
let zipCode: String // eg. 95014
let country: String // eg. United States
let isoCountryCode: String // eg. US
var formattedAddress: String {
return """
\(streetNumber) \(streetName),
\(city), \(state) \(zipCode)
\(country)
"""
}
// Handle optionals as needed
init(with placemark: CLPlacemark) {
self.streetName = placemark.thoroughfare ?? ""
self.streetNumber = placemark.subThoroughfare ?? ""
self.city = placemark.locality ?? ""
self.state = placemark.administrativeArea ?? ""
self.zipCode = placemark.postalCode ?? ""
self.country = placemark.country ?? ""
self.isoCountryCode = placemark.isoCountryCode ?? ""
}
}
For testing purposes, I called my main view file Test.swift. Here's a stripped down version for reference.
Test.swift
import SwiftUI
import Combine
import CoreLocation
import MapKit
struct Test: View {
#StateObject private var mapSearch = MapSearch()
func reverseGeo(location: MKLocalSearchCompletion) {
let searchRequest = MKLocalSearch.Request(completion: location)
let search = MKLocalSearch(request: searchRequest)
var coordinateK : CLLocationCoordinate2D?
search.start { (response, error) in
if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate {
coordinateK = coordinate
}
if let c = coordinateK {
let location = CLLocation(latitude: c.latitude, longitude: c.longitude)
CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
guard let placemark = placemarks?.first else {
let errorString = error?.localizedDescription ?? "Unexpected Error"
print("Unable to reverse geocode the given location. Error: \(errorString)")
return
}
let reversedGeoLocation = ReversedGeoLocation(with: placemark)
address = "\(reversedGeoLocation.streetNumber) \(reversedGeoLocation.streetName)"
city = "\(reversedGeoLocation.city)"
state = "\(reversedGeoLocation.state)"
zip = "\(reversedGeoLocation.zipCode)"
mapSearch.searchTerm = address
isFocused = false
}
}
}
}
// Form Variables
#FocusState private var isFocused: Bool
#State private var btnHover = false
#State private var isBtnActive = false
#State private var address = ""
#State private var city = ""
#State private var state = ""
#State private var zip = ""
// Main UI
var body: some View {
VStack {
List {
Section {
Text("Start typing your street address and you will see a list of possible matches.")
} // End Section
Section {
TextField("Address", text: $mapSearch.searchTerm)
// Show auto-complete results
if address != mapSearch.searchTerm && isFocused == false {
ForEach(mapSearch.locationResults, id: \.self) { location in
Button {
reverseGeo(location: location)
} label: {
VStack(alignment: .leading) {
Text(location.title)
.foregroundColor(Color.white)
Text(location.subtitle)
.font(.system(.caption))
.foregroundColor(Color.white)
}
} // End Label
} // End ForEach
} // End if
// End show auto-complete results
TextField("City", text: $city)
TextField("State", text: $state)
TextField("Zip", text: $zip)
} // End Section
.listRowSeparator(.visible)
} // End List
} // End Main VStack
} // End Var Body
} // End Struct
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
If anyone is wondering how to generate global results, change the code from this:
self.locationResults = results.filter{$0.subtitle.contains("United States")}
to this in Address Structure file:
self.locationResults = results
Related
In the following code I have a List of Cars and each Car from that list has its own list of Services, I can add and delete Cars without a problem by calling carViewModel.addNewCar(make:String, model:String) and carViewModel.deleteCar(at indexSet:IndexSet).
Car.swift
import RealmSwift
final class Car: Object, ObjectKeyIdentifiable{
#objc dynamic var make: String = ""
#objc dynamic var model: String = ""
// creation date, ID etc.
dynamic var services = List<CarService>()
}
CarList.swift
import RealmSwift
final class CarList: Object, ObjectKeyIdentifiable{
#objc dynamic var name: String = ""
// creation date, ID etc.
var cars = RealmSwift.List<Car>()
}
CarService.swift
import RealmSwift
final class CarService: Object, ObjectKeyIdentifiable{
#objc dynamic var serviceName: String = ""
// creation date, ID etc.
}
View Model
import RealmSwift
class CarViewModel: ObservableObject{
#Published var cars = List<Car>()
#Published var selectedCarList: CarList? = nil
var token: NotificationToken? = nil
init(){
// Create a the default lists if they don't already exist.
createDefaultCarList()
createDefaultServiceList()
// Initialize the SelectedCarList and the cars variables items from the Default Car List.
if let list = realm?.objects(CarList.self).first{
self.selectedCarList = list
self.cars = list.cars
}
token = selectedCarList?.observe({ [unowned self] (changes) in
switch changes{
case .error(_): break
case.change(_, _):self.objectWillChange.send()
case.deleted: self.selectedCarList = nil
}
})
}
func addNewCar(make:String, model:String){
if let realm = selectedCarList?.realm{
try? realm.write{
let car = Car()
car.make = make
car.model = model
selectedCarList?.cars.append(car)
}
}
}
func deleteCar(at indexSet:IndexSet){
if let index = indexSet.first,
let realm = cars[index].realm{
try? realm.write{
realm.delete(cars[index])
}
}
}
func addService(toCar: Car, serviceName: String){
try? realm?.write{
let service = CarService()
service.serviceName = serviceName
toCar.services.append(service)
}
}
/// Creates the Default Car List if it doesn't already exists otherwise just prints the error.
func createDefaultCarList(){
do {
if (realm?.objects(CarList.self).first) == nil{
try realm?.write({
let defaultList = CarList()
defaultList.name = "Default Car List"
realm?.add(defaultList)
})
}
}catch let error{
print(error.localizedDescription)
}
}
/// Creates the Default Serivice List if it doesn't already exists otherwise just prints the error.
func createDefaultServiceList(){
do {
if (realm?.objects(ServiceList.self).first) == nil{
try realm?.write({
let defaultList = ServiceList()
defaultList.listName = "Default Service List"
realm?.add(defaultList)
})
}
}catch let error{
print(error.localizedDescription)
}
}
}
My issue is adding or deleting Services to existing Cars. When I call carViewModel.addService(toCar: Car, serviceName: String) I get the error below...
Calling the addService() method.
struct NewServiceFormView: View {
#ObservedObject var carViewModel: CarViewModel
#State var selectedCar:Car // pass from other cars view
var body: some View {
NavigationView {
Form {
// fields
}
.navigationBarItems( trailing:Button("Save", action: addNewCar))
}
}
func addNewCar(){
carViewModel.addService(toCar: selectedCar, serviceName: "Oil Change")
}
}
Error
"Cannot modify managed RLMArray outside of a write transaction."
I can add new Services by explicitly selecting a Car from the cars list. I don't get any errors but the UI doesn't update; I don't see the newly added Service until the app is relaunched.
No errors doing it this way but the UI doesn't update.
carViewModel.addService(toCar: carViewModel.cars[1], serviceName: "Rotors")
How can I properly watch, delete and add Services to existing Cars?
EDIT: Added the following code per Mahan's request.
View to present the NewServiceFormView
struct CarServicesView: View {
#State var selectedCar:Car // a car from parent view
#ObservedObject var carViewModel: CarViewModel
var body: some View {
VStack{
List {
Section(header: Text("Services: \(selectedCar.services.count)")) {
ForEach(selectedCar.services) { service in
}
}
}
.listStyle(GroupedListStyle())
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: openNewServiceForm) {
Image(systemName: "plus")
}
}
}
}.sheet(isPresented: $newServiceFormIsPresented){
NewServiceFormView(carViewModel: carViewModel, selectedCar: selectedCar)
}
}
func openNewServiceForm() {
newServiceFormIsPresented.toggle()
}
}
One issue is how the Realm objects are being observed - remember they are ObjC objects under the hood so you need to use Realm observers. So this
#ObservedObject var carViewModel: CarViewModel
should be this
#ObservedRealmObject var carViewModel: CarViewModel
See the documentation for observedRealmObject
Also, keep in mind if you're observing a Results, the same thing applies, use
#ObservedResults
as shown in the documentation
I want to develop view that's loading data from Health kit (mindfulness time) so I used Timer every 1 minute to get a new data from Health kit, created by Apple watch but onReceive(Timer) are not refreshing a new data (it pass previous data only)
if I Open another app and come back to this app then it's show me a new data
import SwiftUI
struct LoadingView: View {
var healthStore : HealthStore? = HealthStore()
#State private var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State var num : Int = 0
#Binding var showModal: Bool
var decription : String
// MARK: - BODY
var body: some View {
VStack (alignment: .center){
Spacer()
WatchView()
Spacer()
Text(decription)
.font(.callout)
.fontWeight(.semibold)
.padding(.horizontal,30)
Spacer()
}
.navigationBarBackButtonHidden(true)
.onReceive(timer) { _ in
if let healthStore = healthStore {
healthStore.requestAuthorization { success in
if success {
healthStore.getDailyMindfulnessTime { time in
print("\(time)")
}
} //: SUCCESS
}
}
}// ON RECEIVE
.onDisappear(perform: {
self.timer.upstream.connect().cancel()
})//ON DISAPPEAR
}
}
Health Store
import Foundation
import HealthKit
class HealthStore {
var healthStore : HKHealthStore?
var query : HKStatisticsCollectionQuery?
var querySampleQuery : HKSampleQuery?
init(){
// to check data is avaliable or not?
if HKHealthStore.isHealthDataAvailable(){
//Create instance of HKHealthStore
healthStore = HKHealthStore()
}
}
// Authorization
func requestAuthorization(compleion: #escaping(Bool)-> Void){
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
let mindfulSampleType = HKSampleType.categoryType(forIdentifier: .mindfulSession)!
guard let healthStore = self.healthStore else { return compleion(false)}
healthStore.requestAuthorization(toShare: [], read: [stepType,mindfulSampleType]) { (success, error) in
compleion(success)
}
}
//Calculate steps count
func calculateSteps(completion : #escaping(HKStatisticsCollection?)->Void){
let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!
let startDate = Calendar.current.date(byAdding: .day,value: -7, to: Date())
let anchorDate = Date.mondayAt12AM()
let daily = DateComponents(day:1)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date()
, options: .strictStartDate)
//cumulativeSum (Watch+Iphone)
query = HKStatisticsCollectionQuery(quantityType: stepType, quantitySamplePredicate: predicate, options: .cumulativeSum, anchorDate: anchorDate, intervalComponents: daily)
query!.initialResultsHandler = { query, statisticsCollection , error in
completion(statisticsCollection)
}
if let healthStore = self.healthStore, let query = self.query {
healthStore.execute(query)
}
}
// DailyMindfulnessTime
func getDailyMindfulnessTime(completion: #escaping (TimeInterval) -> Void) {
let sampleType = HKSampleType.categoryType(forIdentifier: .mindfulSession)!
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
let startDate = Calendar.current.startOfDay(for: Date())
let endDate = Calendar.current.date(byAdding: .day, value: 1, to: startDate)
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
querySampleQuery = HKSampleQuery(sampleType: sampleType, predicate: predicate, limit: HKObjectQueryNoLimit, sortDescriptors: [sortDescriptor]) { (_, results, error) in
if error != nil {
print(" HealthKit returned error while trying to query today's mindful sessions. The error was: \(String(describing: error?.localizedDescription))")
}
if let results = results {
var totalTime = TimeInterval()
for result in results {
totalTime += result.endDate.timeIntervalSince(result.startDate)
}
completion(totalTime)
} else {
completion(0)
}
}
if let healthStore = self.healthStore, let querySampleQuery = self.querySampleQuery {
healthStore.execute(querySampleQuery)
}
}
}
extension Date {
static func mondayAt12AM() -> Date{
return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear,.weekOfYear],from: Date()))!
}
}
first of all you write in your question that you want to update every minute, but currently you update every second. TimeInterval is a typealias for Double and you pass it in your Timer as seconds. So in your case it should be:
#State private var timer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
Be aware this means onReceive is called after 60 seconds and not immediately.
And I tested your code and it worked fine for me. Unfortunately you didnt include your watch view so I dont know what you are doing in there.
I assumed num is the variable you wanted to update, so you need to call:
num += Int(time)
in your closure for getDailyMindfulnessTime.
If you want to display the time in your WatchView make sure to pass num as a Binding in there.
I'm storing ~100.000 dictionary entries in a realm database and would like to display them. Additionally I want to filter them by a search field. Now I'm running in a problem: The search function is really inefficient although I've tried to debounce the search.
View Model:
class DictionaryViewModel : ObservableObject {
let realm = DatabaseManager.sharedInstance
#Published var entries: Results<DictionaryEntry>?
#Published var filteredEntries: Results<DictionaryEntry>?
#Published var searchText: String = ""
#Published var isSearching: Bool = false
var subscription: Set<AnyCancellable> = []
init() {
$searchText
.debounce(for: .milliseconds(800), scheduler: RunLoop.main) // debounces the string publisher, such that it delays the process of sending request to remote server.
.removeDuplicates()
.map({ (string) -> String? in
if string.count < 1 {
self.filteredEntries = nil
return nil
}
return string
})
.compactMap{ $0 }
.sink { (_) in
} receiveValue: { [self] (searchField) in
filter(with: searchField)
}.store(in: &subscription)
self.fetch()
}
public func fetch(){
self.entries = DatabaseManager.sharedInstance.fetchData(type: DictionaryEntry.self).sorted(byKeyPath: "pinyin", ascending: true)
self.filteredEntries = entries
}
public func filter(with condition: String){
self.filteredEntries = self.entries?.filter("pinyin CONTAINS[cd] %#", searchText).sorted(byKeyPath: "pinyin", ascending: true)
}
In my View I'm just displaying the filteredEtries in a ScrollView
The debouncing works well for short text inputs like "hello", but when I filter for "this is a very long string" my UI freezes. I'm not sure whether something with my debounce function is wrong or the way I handle the data filtering in very inefficient.
EDIT: I've noticed that the UI freezes especially when the result is empty.
EDIT 2:
The .fetchData() function is just this here:
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = realm.objects(type)
return results
}
All realm objects have a primary key. The structure looks like this:
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var character: String = ""
#objc dynamic var pinyin: String = ""
#objc dynamic var translation: String = ""
override class func primaryKey() -> String {
return "id"
}
EDIT 3: The filtered results are displayed this way:
ScrollView{
LazyVGrid(columns: gridItems, spacing: 0){
if (dictionaryViewModel.filteredEntries != nil) {
ForEach(dictionaryViewModel.filteredEntries!){ entry in
Text("\(entry.translation)")
}
} else {
Text("No results found")
}
}
I have a view that listens to a Model via and ObservableObject:
class Feed : ObservableObject {
// Posts to be displayed
#Published var posts = [Posts]()
...
...
}
And the Posts model looks like:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
}
Which contains multiple other Struct models like Friends and Bars. However, when I do change a value within one of these other models, it doesn't trigger the #Published to fire, so the view isn't redrawn. For example, the Friends model looks like:
struct Friends : Hashable {
static func == (lhs: Friends, rhs: Friends) -> Bool {
return lhs.id == rhs.id
}
let name: String
let username: String
let id : String
var thumbnail : UIImage?
var profileImgURL : String?
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
but when I change the thumbnail, the views are not redrawn. But when I change something directly apart of the Posts model, like the description attribute, the view is redrawn. How am I able to have the view redraw when the underlying model values are changed?
I change the thumbnail as shown:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
}
}
}
}
But if I were to change the description doing the same thing:
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
And when I do this, the view does update and change. I can see that the thumbnails are being loaded correctly, too, because I can print out the data sent, and sometimes the thumbnails are redrawn for the view correctly.
EDIT
As suggested I tried adding a mutating func to the struct:
struct Posts: Hashable, Identifiable {
let bar: Bars
let time: String
var description: String
let id: String
let createdAt : String
let tags : [Friends]
let groups : [String]
var intializer : Friends // Creator of the post
mutating func addInitThumbnail(img : UIImage) {
self.intializer.thumbnail = img
}
}
and then using it:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
if let thumbnailImg = UIImage(data: data) {
self.posts[indexOfPost].addInitThumbnail(img: thumbnailImg)
}
}
}
}
}
}
but it did not work either.
However, when I do:
func grabInitThumbnail(post : Posts) {
// Grab the thumbnail of user (if exists)
if post.intializer.profileImgURL != nil {
AF.request(post.intializer.profileImgURL!, method: .get, encoding: URLEncoding.default)
.validate()
.responseData { (response) in
if let data = response.value {
// Find the index of where this post is in the array and set the profile img
if let indexOfPost = self.posts.firstIndex(of: post) {
self.posts[indexOfPost].intializer.thumbnail = UIImage(data: data)
self.posts[indexOfPost].description = "Loaded!!!!"
}
}
}
}
}
the images are loaded and set correctly...? So I think it might have something to do with UIImages directly?
I tried using mutating function and also updating value directly, both cases it worked.
UPDATED CODE (Added UIImage in new struct)
import SwiftUI
import Foundation
//Employee
struct Employee : Identifiable{
var id: String = ""
var name: String = ""
var address: Address
var userImage: UserIcon
init(name: String, id: String, address: Address, userImage: UserIcon) {
self.id = id
self.name = name
self.address = address
self.userImage = userImage
}
mutating func updateAddress(with value: Address){
address = value
}
}
//User profile image
struct UserIcon {
var profile: UIImage?
init(profile: UIImage) {
self.profile = profile
}
mutating func updateProfile(image: UIImage) {
self.profile = image
}
}
//Address
struct Address {
var houseName: String = ""
var houseNumber: String = ""
var place: String = ""
init(houseName: String, houseNumber: String, place: String) {
self.houseName = houseName
self.houseNumber = houseNumber
self.place = place
}
func getCompleteAddress() -> String{
let addressArray = [self.houseName, self.houseNumber, self.place]
return addressArray.joined(separator: ",")
}
}
//EmployeeViewModel
class EmployeeViewModel: ObservableObject {
#Published var users : [Employee] = []
func initialize() {
self.users = [Employee(name: "ABC", id: "100", address: Address(houseName: "Beautiful Villa1", houseNumber: "17ABC", place: "USA"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "XYZ", id: "101", address: Address(houseName: "Beautiful Villa2", houseNumber: "18ABC", place: "UAE"), userImage: UserIcon(profile: UIImage(named: "discover")!)),
Employee(name: "QWE", id: "102", address: Address(houseName: "Beautiful Villa3", houseNumber: "19ABC", place: "UK"), userImage: UserIcon(profile: UIImage(named: "discover")!))]
}
func update() { //both below cases worked
self.users[0].address.houseName = "My Villa"
//self.users[0].updateAddress(with: Address(houseName: "My Villa", houseNumber: "123", place: "London"))
self.updateImage()
}
func updateImage() {
self.users[0].userImage.updateProfile(image: UIImage(named: "home")!)
}
}
//EmployeeView
struct EmployeeView: View {
#ObservedObject var vm = EmployeeViewModel()
var body: some View {
NavigationView {
List {
ForEach(self.vm.users) { user in
VStack {
Image(uiImage: user.userImage.profile!)
Text("\(user.name) - \(user.address.getCompleteAddress())")
}
}.listRowBackground(Color.white)
}.onAppear(perform: fetch)
.navigationBarItems(trailing:
Button("Update") {
self.vm.update()
}.foregroundColor(Color.blue)
)
.navigationBarTitle("Users", displayMode: .inline)
}.accentColor(Color.init("blackTextColor"))
}
func fetch() {
self.vm.initialize()
}
}
it's been a long time but still :
1 - mutating func is not necessary.
2 - The re-rendering won't happen if you only change the nested object and not the "observed" object it self.
3 - You can play with the getters and setters as well, to pull the wanted value to change and update it back.
Considering we have a complex object such as :
struct Content{
var listOfStuff : [Any] = ["List", 2, "Of", "Stuff"]
var isTheSkyGrey : Bool = false
var doYouLikeMyMom : Bool = false
var status : UIImage? = UIImage(systemName: "paperplane")
}
Now let's wrap/nest this object into a ContentModel for the View. If, while using the #State var contentModel : ContentModel in the View, we change change one of the properties directly by accessing the nested object(like so : model.content.status = "Tchak"), it will not trigger a re-rendering because the ContentModel itself didn't change.
Understanding this, we need to trigger a tiny useless change in the ContentModel :
struct ContentModel {
private var change : Bool = false
private var content : Content = Content() {
didSet{
// this will trigger the view to re-render
change.toggle()
}
}
//the value you want to change
var status : UIImage?{
get{
contentModel.status
}
set{
contentModel.status = newValue
}
}
}
Now what's left to do is to observe the change of the content in the view.
struct ContentPouf: View {
#State var contentModel = ContentModel()
var body: some View {
Image(uiImage: contentModel.status)
.onTapGesture {
contentModel.status = UIImage(systemName: "pencil")
}
}
}
and using an ObservableObject it would be :
class ContentObservable : ObservableObject {
#Published var content : ContentModel = ContentModel()
func handleTap(){
content.status = UIImage(systemName: "pencil")
}
}
and
#StateObject var viewModel : ContentObservable = ContentObservable()
var body: some View {
Image(uiImage :viewModel.content.status)
.onTapGesture {
viewModel.handleTap()
}
}
I am exploring SwiftUI+Combine with a demo app BP Management.
Homescreen has a provision to take bp readings(systolicBP, diastolicBP, pulse & weight).
Button "Next" is enabled only when all 4 fields are filled.
control should fall to the next textfield when a valid input is entered. (input is valid when it falls between the range specified by the placeholder - refer the image below)
On tapping next, on the detail screen user can edit the bp values (taken in the HomeScreen), additionally he can add recorded date, notes...
Thought enums would be best model this so I proceeded like
enum SBPInput: CaseIterable {
//name is a Text to indicate the specific row
typealias Field = (name: String, placeholder: String)
case spb, dbp, pulse, weight, note, date
var field: Field {
switch self {
case .dbp: return ("DBP", "40-250")
case .spb: return ("SBP", "50-300")
case .pulse: return ("Pulse", "40-400")
case .weight: return ("Weight", "30-350")
case .note: return ("Note", "")
case .date: return ("", Date().description)
}
}
// Here I am getting it wrong, - I can't bind a read only property
var value: CurrentValueSubject<String, Never> {
switch self {
case .date:
return CurrentValueSubject<String, Never>(Date().description)
case .spb:
return CurrentValueSubject<String, Never>("")
case .dbp:
return CurrentValueSubject<String, Never>("")
case .pulse:
return CurrentValueSubject<String, Never>("")
case .weight:
return CurrentValueSubject<String, Never>("70")
case .note:
return CurrentValueSubject<String, Never>("")
}
}
}
class HomeViewModel: ObservableObject {
#Published var aFieldsisEmpty: Bool = true
var cancellable: AnyCancellable?
var dataSoure = BPInput.allCases
init() {
var bpPublishers = (0...3).map{ BPInput.allCases[$0].value }
//If a field is empty, we need to disable "Next" button
cancellable = Publishers.CombineLatest4(bpPublishers[0], bpPublishers[1], bpPublishers[2], bpPublishers[3]).map { $0.isEmpty || $1.isEmpty || $2.isEmpty || $3.isEmpty }.assign(to: \.aFieldsisEmpty, on: self)
}
}
The idea is to create HStacks for each datasorce(sbp,dbp,pulse,weight) to look like this
struct HomeScreen: View {
#ObservedObject var viewModel = HomeViewModel()
var body: some View {
VStack {
ForEach(Range(0...3)) { index -> BPField in
BPField(input: self.$viewModel.dataSoure[index])
}
Button("Next", action: {
print("Take to the Detail screen")
}).disabled(self.viewModel.aFieldsisEmpty)
}.padding()
}
}
struct BPField: View {
#Binding var input: BPInput
var body: some View {
//implicit HStack
Text(input.field.name)
BPTextField(text: $input.value, placeHolder: input.field.name)//Error:- Cannot assign to property: 'value' is a get-only property
// input.value being read only I can't bind it. How to modify my model now so that I can bind it here?
}
}
And my custom TextField
struct BPTextField: View {
let keyboardType: UIKeyboardType = .numberPad
var style: some TextFieldStyle = RoundedBorderTextFieldStyle()
var text: Binding<String>
let placeHolder: String
// var onEdingChanged: (Bool) -> Void
// var onCommit: () -> ()
var background: some View = Color.white
var foregroundColor: Color = .black
var font: Font = .system(size: 14)
var body: some View {
TextField(placeHolder, text: text)
.background(background)
.foregroundColor(foregroundColor)
.textFieldStyle(style)
}
}
your problems are not there, what SwiftUI tells you.
but you should first compile "small parts" of your code and simplify it, so the compiler will tell you the real errors.
one is here:
BPTextField(text: self.$viewModel.dataSoure[index].value, placeHolder: viewModel.dataSoure[index].field.placeholder)
and the error is:
Cannot subscript a value of type 'Binding<[BPInput]>' with an argument of type 'WritableKeyPath<_, _>'
and of course you forgot the self ....