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.
Related
I have ChatsocketIO class which handles all the SocketIO functions related to the chat functionality of the app. The skeleton of that class looks like below.
My Question is when the Socket receives a message it fires the "socket!.on("msg")", However i am confused how to pass this back to the view controller class which i am calling this API class. I have added the View Controller class as well below..
class ChatServiceAPI {
// MARK: - Properties
var manager: SocketManager? = nil
var socket: SocketIOClient? = nil
//Chat Variables
var items:[Message] = []
// MARK: - Life Cycle
init() {
setupSocket()
setupSocketEvents()
socket?.connect()
}
static let shared = ChatServiceAPI();
func stop() {
socket?.removeAllHandlers()
}
// MARK: - Socket Setup
func setupSocket() {
socket = manager!.defaultSocket;
}
func setupSocketEvents() {
if socket!.status != .connected{
socket!.connect()
}
socket!.on(clientEvent: .connect) { (data, emitter) in
print("==connecting==");
}
socket!.on("msg") { (data, emitter) in
let mesage = Message()
let jsonObject = JSON(data[0])
let messageString: String? = jsonObject["msg"].string
let userIDFrom: Int? = jsonObject["profile"]["id"].int
if(userIDFrom != Int(self.userIDTo)) {
return
}
if( userIDFrom == nil)
{
return
}
mesage.data = JSON(["from_user": self.userIDTo, "to_user": self.userID, "msg": self.convertStringToHtmlCode(msg: messageString ?? ""), "created": Date().description(with: .current)])
self.items.insert(mesage, at: 0)
//self.viewMessagesList.reload(messages: self.items, conversation: self.conversation)
}
socket!.on("disconnect") { (data, emitter) in
print("===disconnect==");
self.isConnected = false
}
socket!.connect();
}
// MARK: - Socket Emits
func register(user: String) {
socket?.emit("add user", user)
}
func send(message: String, toUser: String) {
let data = NSMutableDictionary()
data.setValue(message, forKey: "msg")
data.setValue(toUser.lowercased(), forKey: "to")
socket!.emit("send", data);
return;
}
}
In My View Controller, I have something like below, I want to pass ChatSocketAPI's "self.items" to the below-calling controller, when a msg comes, I am confused about how to do this?
import UIKit
import SocketIO
import SwiftyJSON
import EZAlertController
class MessagingViewController: UIViewController {
let viewMessagesList = MessagesListViewController()
let bottomMenuChatView = BottomMenuChatView(frame: CGRect.zero)
var isAnimation = false
let emojiView = EmojiView(frame: CGRect.zero)
func setupSocketIO() {
ChatServiceAPI.init();
self.socket = ChatServiceAPI.shared.socket;
}
override func viewDidLoad() {
super.viewDidLoad()
self.setupSocketIO()
ChatServiceAPI.shared.page = 0;
ChatServiceAPI.shared.items = []
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setupNavigation()
if Reachability.isConnectedToNetwork() == false {
EZAlertController.alert("", message: "Please check your internet connection")
return
}
// bottom menu setup
bottomMenuChatView.btnSendMessage.addTarget(self, action: #selector(tappedBtnSendMessage(btn:)), for: .touchUpInside)
bottomMenuChatView.btnEmoji.addTarget(self, action: #selector(tappedBtnEmoji(btn:)), for: .touchUpInside)
bottomMenuChatView.textFieldMessage.delegate = self
}
Make your MessagingViewController a listener for the ChatServiceAPI. You can implement that like this:
Create a protocol like the following:
protocol MessageListener: AnyObject {
func messageReceived(text: String)
}
and make you controller conform to it:
extension MessagingViewController: MessageListener {
func messageReceived(text: String) {
// do whatever you need with the message
}
}
Then, in the ChatServiceAPI you can create a var named listeners:
private var listeners: [MessageListener] = []
and methods for adding and removing listeners:
func add(listener: MessageListener) {
self.listeners.append(listener)
}
func remove(listener: MessageListener) {
self.listeners.remove(listener)
}
For the last part, in your ChatServiceAPI, when the "msg" event is received, you need to send the message to all of your registered listeners. So, something like this:
socket!.on("msg") { (data, emitter) in
...
for listener in self.listeners {
listener.messageReceived(text: ...)
}
}
Now you also need to register your viewController as a listener. So you would call ChatServiceAPI.shared.add(listener: self) in your viewDidLoad.
Don't forget to also call ChatServiceAPI.shared.remove(listener: self) to prevent memory leaks.
You can back data to your view controller by writing socket.ON inside a function that have escaping block.
Here is my sample code that I use
func getResponse(completion: #escaping(_ resMessage: String) -> Void) {
guard let socket = manager?.defaultSocket else {
return
}
socket.on(myEventName) { (dataArray, socketAck) -> Void in
guard let data = UIApplication.jsonData(from: dataArray[0]) else {
return
}
do {
let responseMsg = try JSONDecoder().decode(String.self, from: data)
completion(responseMsg)
} catch let error {
print("Something happen wrong here...\(error)")
completion("")
}
}
}
Then you can call this function getResponse in viewDidLoad inside your viewController. Like this -
self.socketInstance.getResponse { socketResponse in
// socketResponse is the response
// this will execute when you get new response from socket.ON
}
I am learning Swift to develop macOS applications and I ran into a problem. I am trying to get certain data from a JSON from the internet. I have managed to get such data and put it in simple text labels in the view but when I run Xcode and get the values, if the values from the JSON get updated, I can't see it reflected in my app. I know that I must perform a function to refresh the data but what I have always found is the function to refresh the data that is in a table, not a simple text label.
Regardless of this problem, if I wanted to add a table with 3 columns (each structure has 3 data, at least) with the values from the JSON. When I run the refresh of the table, I should include in the function the function that gets the data from the internet, right? I'm a bit lost with this too.
This is what I have:
ViewController.swift
import Cocoa
class ViewController: NSViewController, NSTextFieldDelegate {
let user_items = UserItems()
#IBOutlet var item_salida: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
let struc_item = user_items.Item_Struct()
let position = struc_item.firstIndex(where: { $0.name == "Leanne Graham" })!
print(struc_item[position].state!)
item_salida.stringValue = struc_item[position].state!
} }
Struct.swift
import Foundation
import SwiftyJSON
// MARK: - Dato
struct User: Codable {
var name: String?
var username: String?
var email: String?
}
typealias Datos = [User]
class UserItems {
func Item_Struct() -> Datos {
let urlString = "https://jsonplaceholder.typicode.com/users"
var items_available: [User] = []
if let url = NSURL(string: urlString){
if let data = try? NSData(contentsOf: url as URL, options: []){
let items = try! JSONDecoder().decode([User].self, from: data as Data)
for item in items {
items_available.append(item)
}
}
}
return items_available
}
}
Thanks, a lot!
First of all - as you are learning Swift - please stop using snake_case variable names and also the NS.. classes NSURL and NSData.
Never load data from a remote URL with synchronous Data(contentsOf. It blocks the thread.
You need URLSession and an asynchronous completion handler.
// MARK: - Dato
struct User: Codable {
let name: String
let username: String
let email: String
}
typealias Datos = [User]
class UserItems {
func loadData(completion: #escaping (Datos) -> Void) {
let url = URL(string: "https://jsonplaceholder.typicode.com/users")!
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error { print(error); return }
do {
let items = try JSONDecoder().decode([User].self, from: data!)
completion(items)
} catch {
print(error)
}
}.resume()
}
}
And use it in the controller
class ViewController: NSViewController, NSTextFieldDelegate {
#IBOutlet var itemSalida: NSTextField!
let userItems = UserItems()
override func viewDidLoad() {
super.viewDidLoad()
userItems.loadData { users in
if let position = users.firstIndex(where: { $0.name == "Leanne Graham" }) {
DispatchQueue.main.async {
print(users[position].username)
self.itemSalida.stringValue = users[position].username
}
}
}
}
}
And forget SwiftyJSON. It's not needed anymore in favor of Codable.
I am trying to make a GET from a REST API in swift. When I use the print statement (print(clubs)) I see the expected response in the proper format. But in the VC is gives me an empty array.
Here is the code to talk to the API
extension ClubAPI {
public enum ClubError: Error {
case unknown(message: String)
}
func getClubs(completion: #escaping ((Result<[Club], ClubError>) -> Void)) {
let baseURL = self.configuration.baseURL
let endPoint = baseURL.appendingPathComponent("/club")
print(endPoint)
API.shared.httpClient.get(endPoint) { (result) in
switch result {
case .success(let response):
let clubs = (try? JSONDecoder().decode([Club].self, from: response.data)) ?? []
print(clubs)
completion(.success(clubs))
case .failure(let error):
completion(.failure(.unknown(message: error.localizedDescription)))
}
}
}
}
and here is the code in the VC
private class ClubViewModel {
#Published private(set) var clubs = [Club]()
#Published private(set) var error: String?
func refresh() {
ClubAPI.shared.getClubs { (result) in
switch result {
case .success(let club):
print("We have \(club.count)")
self.clubs = club
print("we have \(club.count)")
case .failure(let error):
self.error = error.localizedDescription
}
}
}
}
and here is the view controller code (Before the extension)
class ClubViewController: UIViewController {
private var clubs = [Club]()
private var subscriptions = Set<AnyCancellable>()
private lazy var dataSource = makeDataSource()
enum Section {
case main
}
private var errorMessage: String? {
didSet {
}
}
private let viewModel = ClubViewModel()
#IBOutlet private weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.subscriptions = [
self.viewModel.$clubs.assign(to: \.clubs, on: self),
self.viewModel.$error.assign(to: \.errorMessage, on: self)
]
applySnapshot(animatingDifferences: false)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.viewModel.refresh()
}
}
extension ClubViewController {
typealias DataSource = UITableViewDiffableDataSource<Section, Club>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Club>
func applySnapshot(animatingDifferences: Bool = true) {
// Create a snapshot object.
var snapshot = Snapshot()
// Add the section
snapshot.appendSections([.main])
// Add the player array
snapshot.appendItems(clubs)
print(clubs.count)
// Tell the dataSource about the latest snapshot so it can update and animate.
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
func makeDataSource() -> DataSource {
let dataSource = DataSource(tableView: tableView) { (tableView, indexPath, club) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "ClubCell", for: indexPath)
let club = self.clubs[indexPath.row]
print("The name is \(club.name)")
cell.textLabel?.text = club.name
return cell
}
return dataSource
}
}
You need to apply a new snapshot to your table view once you have fetched the clubs. Your current subscriber simply assigns a value to clubs and nothing more.
You can use a sink subscriber to assign the new clubs value and then call applySnapshot. You need to ensure that this happens on the main queue, so you can use receive(on:).
self.subscriptions = [
self.viewModel.$clubs.receive(on: RunLoop.main).sink { clubs in
self.clubs = clubs
self.applySnapshot()
},
self.viewModel.$error.assign(to: \.errorMessage, on: self)
]
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.
I've set some variables as an Object
import UIKit
class SpeedTestResult: NSObject {
var testTime: NSDate?
}
Then in the controller I set this object and pass it to a class to store it:
testResult.testTime = NSDate()
SpeedTestManager().addTestResult(testResult)
I need to store this object and then access the elements within in a view later, This is what I have:
import Foundation
class SpeedTestManager : NSObject {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
print("Printing testResultArray: \(testResultArray)")
}
}
But when I try to print the the object I just get
Printing testResultArray: [<ProjectName.SpeedTestResult: 0x127b85e50>]
How do I access elements within the object and store this object and retrieve it for later use in a view?
class TestResult : NSObject, NSSecureCoding {
var testTime: NSDate?
override init() {
super.init()
}
#objc required init?(coder decoder: NSCoder) {
self.testTime = decoder.decodeObjectForKey("testTime") as? NSDate
}
#objc func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(self.testTime, forKey: "testTime")
}
#objc static func supportsSecureCoding() -> Bool {
return true
}
override var description: String {
return String.init(format: "TestResult: %#", self.testTime ?? "null")
}
}
class SpeedTestManager : NSObject, NSSecureCoding {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
print("Printing testResultArray: \(testResultArray)")
}
override init() {
super.init()
}
#objc func encodeWithCoder(encoder: NSCoder) {
encoder.encodeObject(self.testResultArray, forKey: "testResultArray")
}
#objc required init?(coder decoder: NSCoder) {
self.testResultArray = decoder.decodeObjectForKey("testResultArray") as! [NSObject]
}
#objc static func supportsSecureCoding() -> Bool {
return true
}
override var description: String {
return String.init(format: "SpeedManager: [%#]", self.testResultArray.map({"\($0)"}).joinWithSeparator(","))
}
}
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let testResult = TestResult()
testResult.testTime = NSDate()
let speedManager = SpeedTestManager()
speedManager.addTestResult(testResult)
NSUserDefaults.standardUserDefaults().setObject(NSKeyedArchiver.archivedDataWithRootObject(speedManager), forKey: "speedManager")
NSUserDefaults.standardUserDefaults().synchronize()
if let archivedSpeedManager = NSUserDefaults.standardUserDefaults().objectForKey("speedManager") as? NSData {
let unarchivedSpeedManager = NSKeyedUnarchiver.unarchiveObjectWithData(archivedSpeedManager)
print("SpeedManager: \(unarchivedSpeedManager ?? "null")")
}
else {
print("Failed to unarchive speed manager")
}
}
}
Here is one way you can do it:
import Foundation
class SpeedTestResult: NSObject {
var testTime: NSDate?
}
class SpeedTestManager : NSObject {
var testResultArray = [NSObject]()
func addTestResult(testResult: NSObject) {
testResultArray.append(testResult)
for result in testResultArray {
// This will crash if result is not a SpeedTestResult.
// print((result as! SpeedTestResult).testTime)
// This is better:
if let timeResult = result as? SpeedTestResult
{
print(timeResult.testTime)
}
else
{
print("Not time type...")
}
}
}
}
var testResult = SpeedTestResult()
testResult.testTime = NSDate()
SpeedTestManager().addTestResult(testResult)
This addresses your specific question, but there are some other problems here:
If you are going to store only SpeedTestResult instances in
testResultArray, then why not make it of type
[SpeedTestResutl]()?
If you will store different types of tests in the array, then how do
you find out which type of test an NSObject element represents?
There are ways... In the above code we at least make sure we are not treating a wrong type of object as a SpeedTestResult.
When you do SpeedTestManager().addTestResult(testResult), you don't
keep a reference to the SpeedTestManager instance. The next time
you make the same call, you will be creating a different
SpeedTestManager instance.
This is not really a problem, but SpeedTestManager does not have to
be a sub-class of NSObject, unless you want to use it in
Objective-C.
You probably don't want to print the content of testResultArray in
the addTestResult() method. You could have other methods for
accessing the array.
To add your test results to the same test manager, you could do:
let myTestManager = SpeedTestManager()
myTestManager.addTestResult(testResult)
// create other test results and add them ...