I'm trying to create an app to get some news from an API and i'm using Moya, RxSwift and MVVM.
This is my ViewModel:
import Foundation
import RxSwift
import RxCocoa
public enum NewsListError {
case internetError(String)
case serverMessage(String)
}
enum ViewModelState {
case success
case failure
}
protocol NewsListViewModelInput {
func viewDidLoad()
func didLoadNextPage()
}
protocol MoviesListViewModelOutput {
var newsList: PublishSubject<NewsList> { get }
var error: PublishSubject<String> { get }
var loading: PublishSubject<Bool> { get }
var isEmpty: PublishSubject<Bool> { get }
}
protocol NewsListViewModel: NewsListViewModelInput, MoviesListViewModelOutput {}
class DefaultNewsListViewModel: NewsListViewModel{
func viewDidLoad() {
}
func didLoadNextPage() {
}
private(set) var currentPage: Int = 0
private var totalPageCount: Int = 1
var hasMorePages: Bool {
return currentPage < totalPageCount
}
var nextPage: Int {
guard hasMorePages else { return currentPage }
return currentPage + 1
}
private var newsLoadTask: Cancellable? { willSet { newsLoadTask?.cancel() } }
private let disposable = DisposeBag()
// MARK: - OUTPUT
let newsList: PublishSubject<NewsList> = PublishSubject()
let error: PublishSubject<String> = PublishSubject()
let loading: PublishSubject<Bool> = PublishSubject()
let isEmpty: PublishSubject<Bool> = PublishSubject()
func getNewsList() -> Void{
print("sono dentro il viewModel!")
NewsDataService.shared.getNewsList()
.subscribe { event in
switch event {
case .next(let progressResponse):
if progressResponse.response != nil {
do{
let json = try progressResponse.response?.map(NewsList.self)
print(json!)
self.newsList.onNext(json!)
}
catch _ {
print("error try")
}
} else {
print("Progress: \(progressResponse.progress)")
}
case .error( _): break
// handle the error
default:
break
}
}
}
}
This is my ViewController, where xCode give me the following error when i try to bind to tableNews:
Expression type 'Reactive<_>' is ambiguous without more context
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
#IBOutlet weak var tableNews: UITableView!
let viewModel = DefaultNewsListViewModel()
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
}
private func setupBindings() {
viewModel.newsList.bind(to: tableNews.rx.items(cellIdentifier: "Cell")) {
(index, repository: NewsList, cell) in
cell.textLabel?.text = repository.name
cell.detailTextLabel?.text = repository.url
}
.disposed(by: disposeBag)
}
}
This is the service that get data from API:
import Moya
import RxSwift
struct NewsDataService {
static let shared = NewsDataService()
private let disposable = DisposeBag()
private init() {}
fileprivate let newsListProvider = MoyaProvider<NewsService>()
func getNewsList() -> Observable<ProgressResponse> {
self.newsListProvider.rx.requestWithProgress(.readNewsList)
}
}
I'm new at rxSwift, I followed some documentation but i'd like to know if i'm approaching in the right way. Another point i'd like to know is how correctly bind my tableView to viewModel.
Thanks for the support.
As #FabioFelici mentioned in the comments, UITableView.rx.items(cellIdentifier:) is expecting to be bound to an Observable that contains an array of objects but your NewsListViewModel.newsList is an Observable<NewsList>.
This means you either have to extract the array out of NewsList (assuming it has one) through a map. As in newsList.map { $0.items }.bind(to:...
Also, your MoviesListViewModelOutput should not be full of Subjects, rather it should contain Observables. And I wouldn't bother with the protocols, struts are fine.
Also, your view model is still very imperative, not really in an Rx style. A well constructed Rx view model doesn't contain functions that are repeatedly called. It just has a constructor (or is itself just a single function.) You create it, bind to it and then you are done.
Related
I'm trying to pass data between viewControllers, but something seems wrong.
The first viewController I want to set the "Bool" to the protocol function to be able to recover in the other screen. What am I doing wrong, I always used protocols but at this time I got in trouble.
That's how I'm doing that:
//
// ComboBoxNode.swift
//
import Foundation
import SWXMLHash
protocol ComboBoxNodeDelegate {
func getCustomOption(data:Bool)
}
class ComboBoxNode: FormControlNode, IFormControlDataSource {
var listType: String?
var dataSource: String?
var dataSourceValue: String?
var dataSourceText: String?
var hasCustomOption:Bool?
var customOptionText: String?
var ctrlDataSourceType: String?
var parameters = [ParameterNode]()
var staticList: FormControlStaticListNode?
var delegate:ComboBoxNodeDelegate?
override init(indexer: XMLIndexer) {
super.init(indexer: indexer)
guard let element = indexer.element else {
preconditionFailure("Error")
}
let isCustomOption = element.bool(by: .hasCustomOption) ?? hasCustomOption
if isCustomOption == true {
self.delegate?.getCustomOption(data: hasCustomOption!)
}
self.readFormControlDataSource(indexer: indexer)
}
override func accept<T, E: IViewVisitor>(visitor: E) -> T where E.T == T {
return visitor.visit(node: self)
}
}
That's how I'm trying to recover on next screen:
// FormPickerViewDelegate.swift
import Foundation
import ViewLib
import RxSwift
class FormPickerViewDelegate: NSObject {
var items = Variable([(value: AnyHashable, text: String)]()) {
didSet {
PickerNodeDelegate = self
self.setDefaultValues()
}
}
private var controlViewModel: FormControlViewModel
private var customText:Bool?
private var PickerNodeDelegate:ComboBoxNodeDelegate?
init(controlViewModel: FormControlViewModel) {
self.controlViewModel = controlViewModel
}
func getItemByValue(_ value: Any) -> (AnyHashable, String)? {
if value is AnyHashable {
let found = items.value.filter {$0.value == value as! AnyHashable}
if found.count >= 1 {
return found[0]
}
}
return nil
}
}
extension FormPickerViewDelegate:ComboBoxNodeDelegate {
func getCustomOption(data: Bool) {
customText = data
}
}
Instead of setting PickerNodeDelegate = self in didSet {} closure
var items = Variable([(value: AnyHashable, text: String)]()) {
didSet {
PickerNodeDelegate = self
self.setDefaultValues()
}
}
Assign it in your init() function instead
init(controlViewModel: FormControlViewModel) {
self.controlViewModel = controlViewModel
PickerNodeDelegate = self
}
Note, your should declare your delegate to be weak also, since it's a delegate, your protocol should conform to be a class type in order to be weakified.
protocol ComboBoxNodeDelegate: class
...
weak var delegate: ComboBoxNodeDelegate?
Here is an example, hope it helps!
protocol ComboBoxNodeDelegate {
func getCustomOption(data:Bool) -> String
}
class ViewOne:ComboBoxNodeDelegate {
var foo:Bool = false
var bar:String = "it works!"
/** Return: String */
func getCustomOption(data:Bool) -> String { //conform here to protocol
// do whatever you wanna do here ...example
self.foo = data // you can set
return bar // even return what you want
}
//initialize
func initalizeViewTwo() {
let v2 = ViewTwo()
v2.delegate = self //since `self` conforms to the ComboBoxNodeDelegate protcol you are allowed to set
}
}
class ViewTwo {
var delegate:ComboBoxNodeDelegate?
func getCustomOption_forV1() {
let view2_foo = delegate.getCustomOption(data:true)
print(view2_foo) // should print "it works!"
}
}
All parameters passed around in Swift are constants -- so you cannot change them.
If you want to change them in a function, you must declare your protocol to pass by reference with inout:
protocol ComboBoxNodeDelegate {
func getCustomOption(data: inout Bool)
}
Note: you cannot pass a constant (let) to this function. It must be a variable -- which I see you are doing!
I'm trying to follow https://github.com/sergdort/CleanArchitectureRxSwift building an app with RxSwift and MVVM.
I can't wrap my head around how to communicate with a child view controller in this scenario.
I have one trigger from the ParentViewController that is needed in the ChildViewModel and a second one from the ChildViewModel that should trigger some UI changes in the ParentViewController:
trigger1: ParentViewController (input)> ... (no idea)> ChildViewModel
trigger2: ChildViewModel (output)> ... (no idea)> ParentViewController
My code currently looks something like this:
final class ParentViewController: UIViewController {
var viewModel: ParentViewModel?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
guard let viewModel = self.viewModel else {
fatalError("View Model not set!")
}
let triggerToChild = rx.someTrigger
let input = ParentViewModel.Input(triggerToChild: triggerToChild)
let output = viewModel.transform(input: input)
output.triggerFromChild
.drive(rx.someProperty)
.disposed(by: disposeBag)
}
}
final class ParentViewModel: ViewModelType {
struct Input {
let triggerToChild: Driver<Void>
}
struct Output {
let triggerFromChild: Driver<Void>
}
func transform(input: Input) -> Output {
let triggerFromChild = ??? <===================
return Output(triggerFromChild: triggerFromChild)
}
}
final class ChildViewController: UIViewController {
var viewModel: ChildViewModel?
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
}
private func bindViewModel() {
guard let viewModel = self.viewModel else {
fatalError("View Model not set!")
}
let triggerFromParent = ??? <===================
let input = ChildViewModel.Input(triggerFromParent: triggerFromParent)
let output = viewModel.transform(input: input)
output.triggerFromParent
.drive(rx.someProperty)
.disposed(by: disposeBag)
}
}
final class ChildViewModel: ViewModelType {
struct Input {
let triggerFromParent: Driver<Void>
}
struct Output {
let triggerToParent: Driver<Void>
}
func transform(input: Input) -> Output {
let triggerToParent = rx.someTrigger
return Output(triggerToParent: triggerToParent)
}
}
Maybe someone could point me in the right direction? Thank You!
I am new to MVC design pattern. I created "DataModel" it will make an API call, create data, and return data to the ViewController using Delegation and "DataModelItem" that will hold all data. How to call a DataModel init function in "requestData" function. Here is my code:
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[DataModelItem])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
func requestData() {
}
private func setDataWithResponse(response:[AnyObject]){
var data = [DataModelItem]()
for item in response{
if let tableViewModel = DataModelItem(data: item as? [String : String]){
data.append(tableViewModel)
}
}
delegate?.didRecieveDataUpdata(data: data)
}
}
And for DataModelItem:
class DataModelItem{
var name:String?
var id:String?
init?(data:[String:String]?) {
if let data = data, let serviceName = data["name"] , let serviceId = data["id"] {
self.name = serviceName
self.id = serviceId
}
else{
return nil
}
}
}
Controller:
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
How to implement simple MVC design pattern in Swift?
As a generic answer, in iOS development you're already doing this implicitly! Dealing with storyboard(s) implies the view layer and controlling the logic of how they work and how they are connected to the model is done by creating view controller, that's the default flow.
For your case, let's clarify a point which is: according to the standard MVC, by default the responsible layer for calling an api should be -logically- the view controller. However for the purpose of modularity, reusability and avoiding to create massive view controllers we can follow the approach that you are imitate, that doesn't mean that its the model responsibility, we can consider it a secondary helper layer (MVC-N for instance), which means (based on your code) is DataModel is not a model, its a "networking" layer and DataModelItem is the actual model.
How to call a DataModel init function in "requestData" function
It seems to me that it doesn't make scene. What do you need instead is an instance from DataModel therefore you could call the desired method.
In the view controller:
let object = DataModel()
object.delegate = self // if you want to handle it in the view controller itself
object.requestData()
I am just sharing my answer here and I am using a codable. It will be useful for anyone:
Model:
import Foundation
struct DataModelItem: Codable{
struct Result : Codable {
let icon : String?
let name : String?
let rating : Float?
let userRatingsTotal : Int?
let vicinity : String?
enum CodingKeys: String, CodingKey {
case icon = "icon"
case name = "name"
case rating = "rating"
case userRatingsTotal = "user_ratings_total"
case vicinity = "vicinity"
}
}
let results : [Result]?
}
NetWork Layer :
import UIKit
protocol DataModelDelegate:class {
func didRecieveDataUpdata(data:[String])
func didFailUpdateWithError(error:Error)
}
class DataModel: NSObject {
weak var delegate : DataModelDelegate?
var theatreNameArray = [String]()
var theatreVicinityArray = [String]()
var theatreiconArray = [String]()
func requestData() {
Service.sharedInstance.getClassList { (response, error) in
if error != nil {
self.delegate?.didFailUpdateWithError(error: error!)
} else if let response = response{
self.setDataWithResponse(response: response as [DataModelItem])
}
}
}
private func setDataWithResponse(response:[DataModelItem]){
for i in response[0].results!{
self.theatreNameArray.append(i.name!)
self.theatreVicinityArray.append(i.vicinity!)
self.theatreiconArray.append(i.icon!)
}
delegate?.didRecieveDataUpdata(data: theatreNameArray)
print("TheatreName------------------------->\(self.theatreNameArray)")
print("TheatreVicinity------------------------->\(self.theatreVicinityArray)")
print("Theatreicon------------------------->\(self.theatreiconArray)")
}
}
Controller :
class ViewController: UIViewController {
private let dataSource = DataModel()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
dataSource.requestData()
}
}
extension ViewController : DataModelDelegate{
func didRecieveDataUpdata(data: [DataModelItem]) {
print(data)
}
func didFailUpdateWithError(error: Error) {
print("error: \(error.localizedDescription)")
}
}
APIManager :
class Service : NSObject{
static let sharedInstance = Service()
func getClassList(completion: (([DataModelItem]?, NSError?) -> Void)?) {
guard let gitUrl = URL(string: "") else { return }
URLSession.shared.dataTask(with: gitUrl) { (data, response
, error) in
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let gitData = try decoder.decode(DataModelItem.self, from: data)
completion!([gitData],nil)
} catch let err {
print("Err", err)
completion!(nil,err as NSError)
}
}.resume()
}
}
I would recommend using a singleton instance for DataModel, since this would be a class you would be invoking from many points in your application.
You may refer its documentation at :
Managing Shared resources using singleton
With this you wont need to initialise this class instance every time you need to access data.
My expectation is to add observables on-the-fly (eg: images upload), let them start, and, when I finished dynamically enqueueing everything, wait for all observable to be finished.
Here is my class :
open class InstantObservables<T> {
lazy var disposeBag = DisposeBag()
public init() { }
lazy var observables: [Observable<T>] = []
lazy var disposables: [Disposable] = []
open func enqueue(observable: Observable<T>) {
observables.append(observable)
let disposable = observable
.subscribe()
disposables.append(disposable)
disposable
.addDisposableTo(disposeBag)
}
open func removeAndStop(atIndex index: Int) {
guard observables.indices.contains(index)
&& disposables.indices.contains(index) else {
return
}
let disposable = disposables.remove(at: index)
disposable.dispose()
_ = observables.remove(at: index)
}
open func waitForAllObservablesToBeFinished() -> Observable<[T]> {
let multipleObservable = Observable.zip(observables)
observables.removeAll()
disposables.removeAll()
return multipleObservable
}
open func cancelObservables() {
disposeBag = DisposeBag()
}
}
But when I subscribe to the observable sent by waitForAllObservablesToBeFinished() , all of them are re-executed (which is logic, regarding how Rx works).
How could I warranty that each are executed once, whatever the number of subscription is ?
While writing the question, I got the answer !
By altering the observable through shareReplay(1), and enqueuing and subscribing to this altered observable.. It works !
Here is the updated code :
open class InstantObservables<T> {
lazy var disposeBag = DisposeBag()
public init() { }
lazy var observables: [Observable<T>] = []
lazy var disposables: [Disposable] = []
open func enqueue(observable: Observable<T>) {
let shared = observable.shareReplay(1)
observables.append(shared)
let disposable = shared
.subscribe()
disposables.append(disposable)
disposable
.addDisposableTo(disposeBag)
}
open func removeAndStop(atIndex index: Int) {
guard observables.indices.contains(index)
&& disposables.indices.contains(index) else {
return
}
let disposable = disposables.remove(at: index)
disposable.dispose()
_ = observables.remove(at: index)
}
open func waitForAllObservablesToBeFinished() -> Observable<[T]> {
let multipleObservable = Observable.zip(observables)
observables.removeAll()
disposables.removeAll()
return multipleObservable
}
open func cancelObservables() {
disposeBag = DisposeBag()
}
}
I want to auto-complete the address for the user as same as what google api provides in this link:
https://developers.google.com/maps/documentation/javascript/places-autocomplete?hl=en
How can i implement the same functionality using apple map kit?
I have tried to use the Geo Coder, i wrote this for example:
#IBAction func SubmitGeoCode(sender: AnyObject) {
let address = "1 Mart"
let coder = CLGeocoder()
coder.geocodeAddressString(address) { (placemarks, error) -> Void in
for placemark in placemarks! {
let lines = placemark.addressDictionary?["FormattedAddressLines"] as? [String]
for addressline in lines! {
print(addressline)
}
}
}
}
However the results are very disappointing.
Any Apple APIs available to implement such functionality, or should i head for google api ?
Thank you
Update - I've created a simple example project here using Swift 3 as the original answer was written in Swift 2.
In iOS 9.3 a new class called MKLocalSearchCompleter was introduced, this allows the creation of an autocomplete solution, you simply pass in the queryFragment as below:
var searchCompleter = MKLocalSearchCompleter()
searchCompleter.delegate = self
var searchResults = [MKLocalSearchCompletion]()
searchCompleter.queryFragment = searchField.text!
Then handle the results of the query using the MKLocalSearchCompleterDelegate:
extension SearchViewController: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(completer: MKLocalSearchCompleter) {
searchResults = completer.results
searchResultsTableView.reloadData()
}
func completer(completer: MKLocalSearchCompleter, didFailWithError error: NSError) {
// handle error
}
}
And display the address results in an appropriate format:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let searchResult = searchResults[indexPath.row]
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
cell.textLabel?.text = searchResult.title
cell.detailTextLabel?.text = searchResult.subtitle
return cell
}
You can then use a MKLocalCompletion object to instantiate a MKLocalSearch.Request, thus gaining access to the MKPlacemark and all other useful data:
let searchRequest = MKLocalSearch.Request(completion: completion!)
let search = MKLocalSearch(request: searchRequest)
search.startWithCompletionHandler { (response, error) in
if error == nil {
let coordinate = response?.mapItems[0].placemark.coordinate
}
}
Swift 5 + Combine + (Optionally) SwiftUI solution
There seem to be a number of comments on other solutions wanting a version compatible with more recent versions of Swift. Plus, It seems likely that (as I did), people will need a SwiftUI solution as well.
This builds on previous suggestions, but uses Combine to monitor the input, debounce it, and then provide results through a Publisher.
The MapSearch ObservableObject is easily used in SwiftUI (example provided), but could also be used in non-SwiftUI situations as well.
MapSearch ObservableObject
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
$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) {
//could deal with the error here, but beware that it will finish the Combine publisher stream
//currentPromise?(.failure(error))
}
}
SwiftUI interface, including mapped locations
struct ContentView: View {
#StateObject private var mapSearch = MapSearch()
var body: some View {
NavigationView {
Form {
Section {
TextField("Address", text: $mapSearch.searchTerm)
}
Section {
ForEach(mapSearch.locationResults, id: \.self) { location in
NavigationLink(destination: Detail(locationResult: location)) {
VStack(alignment: .leading) {
Text(location.title)
Text(location.subtitle)
.font(.system(.caption))
}
}
}
}
}.navigationTitle(Text("Address search"))
}
}
}
class DetailViewModel : ObservableObject {
#Published var isLoading = true
#Published private var coordinate : CLLocationCoordinate2D?
#Published var region: MKCoordinateRegion = MKCoordinateRegion()
var coordinateForMap : CLLocationCoordinate2D {
coordinate ?? CLLocationCoordinate2D()
}
func reconcileLocation(location: MKLocalSearchCompletion) {
let searchRequest = MKLocalSearch.Request(completion: location)
let search = MKLocalSearch(request: searchRequest)
search.start { (response, error) in
if error == nil, let coordinate = response?.mapItems.first?.placemark.coordinate {
self.coordinate = coordinate
self.region = MKCoordinateRegion(center: coordinate, span: MKCoordinateSpan(latitudeDelta: 0.03, longitudeDelta: 0.03))
self.isLoading = false
}
}
}
func clear() {
isLoading = true
}
}
struct Detail : View {
var locationResult : MKLocalSearchCompletion
#StateObject private var viewModel = DetailViewModel()
struct Marker: Identifiable {
let id = UUID()
var location: MapMarker
}
var body: some View {
Group {
if viewModel.isLoading {
Text("Loading...")
} else {
Map(coordinateRegion: $viewModel.region,
annotationItems: [Marker(location: MapMarker(coordinate: viewModel.coordinateForMap))]) { (marker) in
marker.location
}
}
}.onAppear {
viewModel.reconcileLocation(location: locationResult)
}.onDisappear {
viewModel.clear()
}
.navigationTitle(Text(locationResult.title))
}
}
My answer is fully based on #George McDonnell's. I hope it helps to guys who has troubles with implementing of the last one.
import UIKit
import MapKit
class ViewController: UIViewController {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableVIew: UITableView!
//create a completer
lazy var searchCompleter: MKLocalSearchCompleter = {
let sC = MKLocalSearchCompleter()
sC.delegate = self
return sC
}()
var searchSource: [String]?
}
extension ViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
//change searchCompleter depends on searchBar's text
if !searchText.isEmpty {
searchCompleter.queryFragment = searchText
}
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return searchSource?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//I've created SearchCell beforehand; it might be your cell type
let cell = self.tableVIew.dequeueReusableCell(withIdentifier: "SearchCell", for: indexPath) as! SearchCell
cell.label.text = self.searchSource?[indexPath.row]
// + " " + searchResult.subtitle
return cell
}
}
extension ViewController: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
//get result, transform it to our needs and fill our dataSource
self.searchSource = completer.results.map { $0.title }
DispatchQueue.main.async {
self.tableVIew.reloadData()
}
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
//handle the error
print(error.localizedDescription)
}
}
SAMPLE PROJECT FOR THIS ISSUE CAN BE DOWNLOAD FROM HERE
In this sample project this issue is achieved through MKLocalSearchRequest and MapKit.
It is showing autocomplete places just like google places API and can place the Annotation point on Apple's map (not on Google map, I hope that is only you are looking for.)
However it does not show as accurate result as you can get from Google Places API. As the problem is that Geocoding database is not obviously complete and Apple is not the company who leads this field - Google is.
Attaching some screenshots of the sample app, so you can see if it is useful for your requirement or not.
Hope this is what you are looking for!
Simple Solution - SwiftUI
How: searchText is linked to Textfield, when Textfield changes, searchText is queried (compared) against worldwide addresses.
The query's completion triggers completerDidUpdateResults which updates the SearchThis.swift list with those results (addresses).
SearchThis.swift (SwiftUI)
import SwiftUI
import Foundation
struct SearchThis : View {
#StateObject var searchModel = SearchModel()
var body: some View {
VStack {
TextField("Type Here", text: $searchModel.searchText)
.onChange(of: searchModel.searchText) { newValue in
searchModel.completer.queryFragment = searchModel.searchText
}
List(searchModel.locationResult, id: \.self) { results in
Button(results.title) {print("hi")}
}
}
}
}
struct SearchThis_Previews: PreviewProvider {
static var previews: some View {
SearchThis()
}
}
SearchModel.swift (Class)
import MapKit
class SearchModel: NSObject, ObservableObject, MKLocalSearchCompleterDelegate {
#Published var searchText = ""
#Published var locationResult: [MKLocalSearchCompletion] = []
let completer = MKLocalSearchCompleter()
override init() {
super.init()
completer.delegate = self
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
locationResult = completer.results
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
print(error.localizedDescription)
}
}