IOS-RxSwift Project Practical Records

Keywords: Swift network JSON Android

Recently, I just finished my OC project. After careful consideration, I decided to use Swift in my next project. It happened that RxSwift had been out for some time, and the grammar was basically stable. So he came to try out RxSwift, and then he made a little Demo. Interested students could have a look at it.~

structure

.
├── Controller
│   └── LXFViewController.swift     // Main View Controller
├── Extension
│   └── Response+ObjectMapper.swift // Response classification, Moya requests Json model or model array
├── Model
│   └── LXFModel.swift              // Model
├── Protocol
│   └── LXFViewModelType.swift      // Model protocol is defined.
├── Tool
│   ├── LXFNetworkTool.swift        // Encapsulating Moya requests
│   └── LXFProgressHUD.swift        // Packaged HUD
├── View
│   ├── LXFViewCell.swift           // Custom cell
│   └── LXFViewCell.xib             // xib file of cell
└── ViewModel
    └── LXFViewModel.swift          // View model

Third party Library

RxSwift         // Necessary libraries for playing RxSwift
RxCocoa         // Rx UIKit Foundation
NSObject+Rx     // Provide us with rx_disposeBag 
Moya/RxSwift    // A network request library dedicated to RxSwift that encapsulates Alamofire
ObjectMapper    // The Necessary Good Product of Json Transform Model
RxDataSources   // Help US gracefully use the tableView data source approach
Then            // Provide quick initialization of grammatical sugar
Kingfisher      // Picture Loading Library
SnapKit         // View Constraint Library
Reusable        // Help US gracefully use custom cell s and view s without Optional
MJRefresh       // Pull-Up Load, Pull-Down Refresh Library
SVProgressHUD   // Easy to use HUD

Blackboard

Use of Moya

Moya is a network request Library Based on Alamofire. Here I use Moya/Swift, which adds interface support for RxSwift on the basis of Moya. Next, let's talk about the use of Moya.

1. Create an enumeration to store the request type. Here I set the corresponding path by the way, and then take it out uniformly and assign it directly.

enum LXFNetworkTool {
    enum LXFNetworkCategory: String {
        case all     = "all"
        case android = "Android"
        case ios     = "iOS"
        case welfare = "welfare"
    }
    case data(type: LXFNetworkCategory, size:Int, index:Int)
}

2. Write an extension for this enumeration and follow the TargetType protocol. Moya, a library protocol of this protocol, can press the Commond key + click the left key to enter the corresponding file for viewing.

extension LXFNetworkTool: TargetType {
    /// baseURL Unify basic URL
    var baseURL: URL {
        return URL(string: "http://gank.io/api/data/")!
    }

    /// path Fields are appended to baseURL behind
    var path: String {
        switch self {
        case .data(let type, let size, let index):
            return "\(type.rawValue)/\(size)/\(index)"
        }
    }

    /// HTTP The mode of request
    var method: Moya.Method {
        return .get
    }

    /// Request parameters(Coding will occur on request)
    var parameters: [String: Any]? {
        return nil
    }

    /// Parametric encoding(Use here URL Default mode)
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }

    /// It's for unit testing, and it doesn't need to be written like I do.
    var sampleData: Data {
        return "LinXunFeng".data(using: .utf8)!
    }

    /// Tasks to be performed(Request: request Download: upload Upload: download)
    var task: Task {
        return .request
    }

    /// Whether to implement Alamofire Verify that the default value is false
    var validate: Bool {
        return false
    }
}

3. Define a global variable for network requests for the entire project

let lxfNetTool = RxMoyaProvider<LXFNetworkTool>()

So far, we can use this global variable to request data.

RxDataSources

It's OK if you want to use the traditional way, but it loses the meaning of using RxSwift. Well, let's talk about how to gracefully implement tableView's data source. actually RxDataSources There are clear instructions on the official website, but let me summarize the whole process.

Concept point
RxDataSources are transmitted by section as data structure, which is very important. Maybe many students will be confused about this sentence. In this case, I have a numberOfSection in the traditional method of data source implementation. In many cases, we only need a section. So this method can be implemented or not. The default return is 1. This is for me. One of the puzzles we have is, [Table View is made up of row s]. I wonder if any of you who are sitting here think that way? If you have only one set, you have to go back to an array of sections!!!

Custom Section
Create a Section structure in our custom Model, and create an extension that follows the Section Model Type protocol to implement the corresponding protocol method. See the following way for a customary way of writing

LXFModel.swift

struct LXFSection {
    // items are rows.
    var items: [Item]
    // You can also add what you need here, such as the title of headerView.
}

extension LXFSection: SectionModelType {

    // Redefining the type of Item to LXFModel
    typealias Item = LXFModel

    // Ways to implement protocols
    init(original: LXFSection, items: [LXFSection.Item]) {
        self = original
        self.items = items
    }
}

2. Create a data source property under the controller

The following code is in the LXFViewController.swift file

// Create a data source property with a custom Selection type
let dataSource = RxTableViewSectionedReloadDataSource<LXFSection>()

Binding our cell with data source properties

// Binding cell
dataSource.configureCell = { ds, tv, ip, item in
    // This place uses the Reusable library and complies with the corresponding protocol in LXFViewCell.
    // Converting cells to non-optional cell types
    let cell = tv.dequeueReusableCell(for: ip) as LXFViewCell
    cell.picView.kf.setImage(with: URL(string: item.url))
    cell.descLabel.text = "describe: \(item.desc)"
    cell.sourceLabel.text = "source: \(item.source)"
    return cell
}

3. Binding the sections sequence to our rows

output.sections.asDriver().drive(tableView.rx.items(dataSource:dataSource)).addDisposableTo(rx_disposeBag)

With great success, let's talk about the generation of section sequence.

Specification of ViewModel

We know that the idea of MVVM is to store the view display logic, validation logic, network request code originally in ViewController in ViewModel, so that our ViewController can be slimmed down. These logics are in the charge of ViewModel, the outside world does not need to care about, the outside world only needs the result, and the ViewModel only needs to give the result to the outside world. Based on this, we define a protocol LXFViewModel Type.

1. Create a LXFViewModelType.swift

LXFViewModelType.swift

// associatedtype Keyword is used to declare placeholders of a type as part of the protocol definition
protocol LXFViewModelType {
    associatedtype Input
    associatedtype Output

    func transform(input: Input) -> Output
}

II. viewModel Compliance with LXFViewModelType Protocol

  1. We can define aliases for Input and Output of XFViewModel Type to distinguish. For example, if your viewModel is associated with the request homepage module, it can be named HomeInput and HomeOutput.
  2. We can enrich our Input and Output. You can see that I added a sequence for Output, a type for our custom LXFSection array, and a request type for Input (that is, what data to request, such as the data on the home page)
  3. We use the transform method to process the data carried by input and generate an Output.

Note: The following code has been partially deleted for ease of reading

LXFViewModel.swift

extension LXFViewModel: LXFViewModelType {
   // An array of parsed models
   let models = Variable<[LXFModel]>([])

    // Define aliases for Input and Output of LXFViewModelType
    typealias Input = LXFInput
    typealias Output = LXFOutput

    // Enriching our Input and Output
    struct LXFInput {
        // Network Request Type
        let category: LXFNetworkTool.LXFNetworkCategory

        init(category: LXFNetworkTool.LXFNetworkCategory) {
            self.category = category
        }
    }

    struct LXFOutput {
        // sections data of tableView
        let sections: Driver<[LXFSection]>

        init(sections: Driver<[LXFSection]>) {
            self.sections = sections
        }
    }

    func transform(input: LXFViewModel.LXFInput) -> LXFViewModel.LXFOutput {
        let sections = models.asObservable().map { (models) -> [LXFSection] in
            // It is called when the values of models are changed, which is a feature of Variable.
            return [LXFSection(items: models)] // Returns the section array
        }.asDriver(onErrorJustReturn: [])

        let output = LXFOutput(sections: sections)

        // The next code is the network request, please view it in conjunction with the project, otherwise it will be inconvenient to read and understand.
    }
}

Then we initialize our input in ViewController, get output through transform ation, and bind the sections sequence in our output to tableView items.

LXFViewController.swift

// Initialize input
let vmInput = LXFViewModel.LXFInput(category: .welfare)
// Get output through transform ation
let vmOutput = viewModel.transform(input: vmInput)

vmOutput.sections.asDriver().drive(tableView.rx.items(dataSource: dataSource)).addDisposableTo(rx_disposeBag)

Using MJRefresh in RxSwift

1. Define an enumeration LXFRefreshStatus to mark the current refresh status

enum LXFRefreshStatus {
    case none
    case beingHeaderRefresh
    case endHeaderRefresh
    case beingFooterRefresh
    case endFooterRefresh
    case noMoreData
}

2. Add a refreshStatus sequence to LXFOutput, of type LXFRefreshStatus

// Subscribe to the outside world and tell the outside world about the current refresh status of the tableView
let refreshStatus = Variable<LXFRefreshStatus>(.none)

After we make the network request and get the result, we modify the value of refreshStatus to the corresponding LXFRefreshStatus item.

3. refreshStatus of Outside Subscribed output

Outside subscribes to refreshStatus of output and performs corresponding operations based on the received values

vmOutput.refreshStatus.asObservable().subscribe(onNext: {[weak self] status in
    switch status {
    case .beingHeaderRefresh:
        self?.tableView.mj_header.beginRefreshing()
    case .endHeaderRefresh:
        self?.tableView.mj_header.endRefreshing()
    case .beingFooterRefresh:
        self?.tableView.mj_footer.beginRefreshing()
    case .endFooterRefresh:
        self?.tableView.mj_footer.endRefreshing()
    case .noMoreData:
        self?.tableView.mj_footer.endRefreshingWithNoMoreData()
    default:
        break
    }
}).addDisposableTo(rx_disposeBag)

4. output provides a request Commond for requesting data

Publish Subject: It can be used as Observable or Observer, in other words, it can send or subscribe to signals.

// This property tells viewModel to load data (the value passed in is to indicate whether or not to reload)
let requestCommond = PublishSubject<Bool>()

In transform, we subscribe to the request Commond of the generated output

output.requestCommond.subscribe(onNext: {[unowned self] isReloadData in
    self.index = isReloadData ? 1 : self.index+1
    lxfNetTool.request(.data(type: input.category, size: 10, index: self.index)).mapArray(LXFModel.self).subscribe({ [weak self] (event) in
        switch event {
        case let .next(modelArr):
            self?.models.value = isReloadData ? modelArr : (self?.models.value ?? []) + modelArr
            LXFProgressHUD.showSuccess("Load successful")
        case let .error(error):
            LXFProgressHUD.showError(error.localizedDescription)
        case .completed:
            output.refreshStatus.value = isReloadData ? .endHeaderRefresh : .endFooterRefresh
        }
    }).addDisposableTo(self.rx_disposeBag)
}).addDisposableTo(rx_disposeBag)

5. Initialize refresh control in ViewController

Set the refresh control for tableView and use output's request Commond to emit signals in the callback to create the refresh control

tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { 
    vmOutput.requestCommond.onNext(true)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { 
    vmOutput.requestCommond.onNext(false)
})

Summary process:

  1. ViewController has got output, and when the data is loaded drop-down, it uses the request Commond of output to send information telling viewModel that we need to load the data.

  2. viewModel requests data, modifies models after processing json to model or model array, and signals sections when the values of models are modified. Sections are bound to tableView items in ViewController, so tableView data is updated. Next, we modify the value of the refreshStatus attribute of output based on the result of the request.

  3. When the value of the refreshStatus attribute of output changes, a signal is emitted. Since the refreshStatus of output has been subscribed to by the outside world, the refreshStatus status of the refreshStatus control is processed according to the new value of refreshStatus.

All right. Attached. RxSwiftDemo . End of the flower

Posted by fr@nkie on Sun, 26 May 2019 10:43:39 -0700