GithubHelp home page GithubHelp logo

hadanischal / starwarscharacters Goto Github PK

View Code? Open in Web Editor NEW
4.0 2.0 0.0 8.84 MB

This is a simple Demo project which aims to demonstate the Star Wars Characters using MVVM pattern in Swift. The demo uses the Star Wars API as an excuse to have a nice use-case, because querying a WebService API is asynchronous by nature and is thus a good example for showing how It can be useful .

Swift 99.30% Ruby 0.70%

starwarscharacters's Introduction

StarWarsCharacters

Requirements:

  • iOS 11.0+
  • Xcode 10.2.1
  • Swift 5.0

Compatibility

This demo is expected to be run using Swift 5.0 and Xcode 10.2.x.

Objective:

This is a simple Demo project which aims to demonstate the Star Wars Characters using MVVM pattern in Swift.

  • This project was intended to work as a Star Wars Characters demo projects for iOS using Swift. It has been structured using the MVVM design pattern.
  • The demo uses the Star Wars API as an excuse to have a nice use-case, because querying a WebService API is asynchronous by nature and is thus a good example for showing how It can be useful .

Specification

Guidelines

  • Shows a list view of all Star Wars characters (people) with their names and eye-color.
  • Add a dynamic control (for example segment control) that filters the list by eye-color of people. This segment control should be scalable for the future, so should automatically show more elements when the api returns more eye-colors some day. Each segment should show the eye color and the amount of people, like:" green (12) "

App Demo

Model

These hold the app data. These are the structs and classes that you have created to hold the data you receive from a REST API or from some other data source.

  • CharactersModel.swift
struct CharactersModel: Codable {
    let count: Int
    let next: String
    let previous: String?
    let results: [PersonModel]?
}

extension CharactersModel: Parceable {
    static func parseObject(data: Data) -> Result<CharactersModel, ErrorResult> {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        if let result = try? decoder.decode(CharactersModel.self, from: data) {
            return Result.success(result)
        } else {
            return Result.failure(ErrorResult.parser(string: "Unable to parse flickr results"))
        }
    }
}
  • PersonModel.swift
struct PersonModel: Codable, Equatable {
    let name, height, mass, hairColor: String
    let skinColor, eyeColor, birthYear: String
    let gender: Gender
    let homeworld: String
    let films, species, vehicles, starships: [String]
    let created, edited: String
    let url: String
}
  • Gender.swift
enum Gender: String, Codable {
    case female = "female"
    case male = "male"
    case notAvailable = "n/a"
}
  • EyeColorModel.swift
struct EyeColorModel: Equatable {
    let eyeColor: String
    let count: Int
    let results: [PersonModel]?
}

extension EyeColorModel {
    static func == (lhs: EyeColorModel, rhs: EyeColorModel) -> Bool {
        return lhs.eyeColor == rhs.eyeColor
            && lhs.count == rhs.count
            && lhs.results == rhs.results
    }
}

ViewModel

To be able to bind values from our ViewModel to our View, we need element with an observable pattern. In iOS, we could use KVO pattern to add and remove observers, but I would prefer RxSwift. KVO observing, async operations and streams are all unified under abstraction of sequence. This is the reason why Rx is so simple, elegant and powerful.

  • PersonViewModelProtocol
protocol PersonViewModelProtocol {
    var onErrorHandling: ((ErrorResult?) -> Void)? { get set }
    func didSelectSegment(_ segmentIndex: Int)
    func fetchServiceCall(_ completion: ((Result<Bool, ErrorResult>) -> Void)?)
    var filteredResults: [EyeColorModel] { get }
}
  • PersonViewModel.swift
import Foundation

final class PersonViewModel: PersonViewModelProtocol {
    // MARK: - Input
    private var service: CharactersRouterProtocol?
    private weak var dataSource: GenericDataSource<PersonModel>?
    private var personHelper: PersonHelperDataSource

    // MARK: - Output
    var filteredResults: [EyeColorModel] = []
    var onErrorHandling: ((ErrorResult?) -> Void)?
    var onFilteredResults: ((EyeColorModel?) -> Void)?

    init(service: CharactersRouterProtocol = CharactersRouter(),
         withPersonHelper personHelper: PersonHelperDataSource = PersonHelper(),
         dataSource: GenericDataSource<PersonModel>?) {
        self.service = service
        self.personHelper = personHelper
        self.dataSource = dataSource
    }

    func fetchServiceCall(_ completion: ((Result<Bool, ErrorResult>) -> Void)? = nil) {
        guard let service = self.service else {
            onErrorHandling?(ErrorResult.custom(string: "Missing service"))
            return
        }
        service.fetchConverter { [weak self] result in
            DispatchQueue.main.async {
                switch result {
                case .success(let converter) :
                    if let results = converter.results {
                        self?.dataSource?.data.value = results
                        self?.filteredResults = self?.personHelper.parseEyeColorArray(results: results) ?? []
                        completion?(Result.success(true))
                    } else {
                        self?.onErrorHandling?(ErrorResult.parser(string: "unable to parse"))
                        completion?(Result.failure(ErrorResult.parser(string: "unable to parse")))
                    }
                case .failure(let error) :
                    self?.onErrorHandling?(error)
                    completion?(Result.failure(error))
                }
            }
        }
    }

    func didSelectSegment(_ segmentIndex: Int) {
        self.dataSource?.data.value = filteredResults[segmentIndex].results ?? []
    }
}
  • PersonDataSource.swift
import Foundation
import UIKit

class GenericDataSource<T>: NSObject {
    var data: DynamicValue<[T]> = DynamicValue([])
}

final class PersonDataSource: GenericDataSource<PersonModel>, UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.value.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(forIndexPath: indexPath) as PeopleTableViewCell
        let data = self.data.value[indexPath.row]
        cell.personModel = data
        return cell
    }

}

View

let’s implement our View, which is EmployeeRosterVC. What’s need to be done there is to link a UITableView to its dataSource, but also to bind values to be able to automatically refresh the UI when new data is available

import UIKit
import Segmentio

class PersonViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var segmentioView: Segmentio!

    fileprivate var activityIndicator: ActivityIndicator! = ActivityIndicator()
    private let refreshControl = UIRefreshControl()
    let dataSource = PersonDataSource()
    lazy var viewModel: PersonViewModelProtocol = {
        let viewModel = PersonViewModel(dataSource: dataSource)
        return viewModel
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        configureTableView()
        setupUIRefreshControl()
        setupViewModel()
        activityIndicator.start()
    }

    private func setupUI() {
        title = "Star Wars characters"
        tableView.backgroundColor = .white
        view.backgroundColor = .white
        tableView.tableFooterView = UIView(frame: CGRect.zero)
        segmentioView.isHidden = true
    }

    private func setupUIRefreshControl() {
        tableView.addSubview(refreshControl)
        refreshControl.addTarget(self, action: #selector(refreshPeopleData), for: .valueChanged)
    }

    private func setupViewModel() {
        tableView.dataSource = self.dataSource

        dataSource.data.addAndNotify(observer: self) { [weak self] _ in
            self?.tableView.reloadData()
        }
        viewModel.onErrorHandling = { [weak self] error in
            self?.activityIndicator.stop()
            DefaultWireframe().presentAlert(self!, title: "An error occured", message: "Oops, something went wrong!")
        }

        viewModel.fetchServiceCall { [weak self] _ in
            self?.activityIndicator.stop()
            self?.setupSegmentioView()
        }
    }

    @objc private func refreshPeopleData(_ sender: Any) {
        activityIndicator.start()
        viewModel.fetchServiceCall { _ in
            self.activityIndicator.stop()
        }
        refreshControl.endRefreshing()
    }
}

extension PersonViewController {
    private func setupSegmentioView() {
        segmentioView.isHidden = false

        let segmentioContent = viewModel.filteredResults.flatMap { result -> [SegmentioItem] in
            return [SegmentioItem(title: result.eyeColor.capitalized, image: nil)]
        }
        SegmentioBuilder.buildSegmentioView(
            segmentioView: segmentioView,
            segmentioStyle: .onlyLabel,
            segmentioContent: segmentioContent
        )

        viewModel.filteredResults.enumerated().forEach { result in
            SegmentioBuilder.setupBadgeCountForIndex(segmentioView, index: result.offset, count: result.element.count)
        }

        segmentioView.selectedSegmentioIndex = 0

        segmentioView.valueDidChange = { [weak self] _, segmentIndex in
            print("Selected item: \(segmentIndex)")
            self?.viewModel.didSelectSegment(segmentIndex)
        }
    }

}

// MARK: - TableView Setup

fileprivate extension PersonViewController {

    func configureTableView() {
        tableView.register(PeopleTableViewCell.self)
        tableView.estimatedRowHeight = 83
        tableView.rowHeight = UITableView.automaticDimension
    }
}

starwarscharacters's People

Contributors

hadanischal avatar

Stargazers

pohoze avatar Mike avatar  avatar Devi Pd. Ghimire  (Dip Kasyap) avatar

Watchers

James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.