preface

Some time ago, I accidentally found zhihu, a zhihu answer and user’s selection station. The developer of the site is Zhihu user Surian, who wrote a crawler to grab data from Zhihu and provided API documentation. I scanned the documentation and thought it would be a good idea to write an iOS client, so I started.

I didn’t use any third party libraries because it was a personal project and the main purpose was to practice. Network requests, JSON parsing, asynchronous image loading and so on are all packaged by themselves. UI layout is mainly done in Storyboard and AutoLayout. The development language is Swift. I’ve done most of the work so far, but it didn’t take long, and I’ll add some features, polish them, and add some notes. Due to the short time, I did not write the test cases, the whole project must still have many deficiencies, if any friends find any bugs, please leave a message and tell me. I’m going to go over here and show you the project in general, and then pick out a few points that I think are worth talking about. I believe to you how much should be some help.

Realize the function

Article recommendation:

“Kanzhihu” recommended answers are based on articles, and three articles are published in three periods of time every day, named yesterday, Recent and Archive. Each article recommends 32 to 40 answers

The client accepts the latest 10 recommendations. If you click on a single recommendation, you will be taken to the corresponding answer list. If you click on a single answer, you will be taken to the corresponding answer details.

User ranking:

Get the list of top 30 users in a certain indicator (number of approval, number of fans), click a single user to go to the user details page.

User details page (display effect imitates jian Shu personal user interface) displays the user’s recent dynamic and high ticket answers, click the specific answer to go to the answer details page. More to be added.

User search: Enter the user name or part of the user name to search directly. The search result displays a list of related users. Click a user to go to the user details page.

Projects show




Homepage. GIF




Home answer list.gif




Answer details.gif




User ranking.gif




User details.gif




The user replies.gif




The user searches.gif




Ranking. GIF




Project structure.png

The project is mainly divided into two modules, namely Home module (Home) and TopUsers module (TopUsers). In the Global directory are a few simple libraries and constants that I’ve wrapped myself.

A few Tips

Use Storyboard to quickly set layer properties




label.png

Setting rounded corners, borders and other attributes is almost a daily thing to do in daily development. For example, we now want to implement the label with a border and rounded corners as above. We can write it in code like this:

label.layer.cornerRadius = xxx
label.layer.borderColor = xxx
label.layer.borderWidth = xxxCopy the code

But if you’re using storyboards (storyboards are actually XML files) for layout, you might not be able to tolerate layout-related code in your logic anymore. What about storyboards? More straightforward is to use Runtime:




Runtime Attributes.png

You can add layer.cornerRadius properties and set Type and Value in the top corner. The layer.borderColor attribute requires a CGColor Type, but only UIColor can be set. So the layer.borderColor attribute does not work.

The best way to do this is with extension and @ibInspectable:

extension UIView { @IBInspectable var cornerRadius: CGFloat { set { layer.cornerRadius = newValue layer.masksToBounds = newValue > 0 } get { return layer.cornerRadius } } @IBInspectable var borderWidth: CGFloat { set { layer.borderWidth = newValue } get { return layer.borderWidth } } @IBInspectable var borderColor: UIColor? { set { layer.borderColor = newValue? .CGColor } get { return layer.borderColor ! = nil ? UIColor(CGColor: layer.borderColor!) : nil } } }Copy the code

The property marked @ibInspectable is displayed in the Storyboard:




Rounded corner label. PNG

Because I’ve extended these properties to UIView, all controls that inherit from UIView can easily set these properties in Storyboard.

The realization of simple book type user personal page

My user details page is written in simple style. In general, the profile picture will slide down along the page (the initial state is half of the profile picture in the navigation bar, and finally the whole profile picture will be in the navigation bar), and then the menu item will stay at the bottom of the navigation bar. Click the menu item and the corresponding data will be displayed in the Cell below.

The head zooming is mainly about changing the width and height constraints and the cornerRadius (to make a square round, just set its cornerRadius to half of the side length) :

Func scrollViewDidScroll(scrollView: UIScrollView) { let offsetY = scrollView.contentOffset.y let headerHeight = tableHeader.frame.height guard offsetY < headerHeight else { avatarHeight.constant = avatarMaxRadius/2 avatarWidth.constant = avatarMaxRadius/2 AvatarImageView. CornerRadius = avatarMaxCornerRadius / 2 return} let multiplier = offsetY headerHeight / / external rectangular eventually reduce half width avatarHeight.constant = avatarMaxRadius - avatarMaxRadius/2 * multiplier avatarWidth.constant = avatarHeight.constant LayoutAvatarImmediately () / / radius eventually reduce half avatarImageView cornerRadius = avatarMaxCornerRadius avatarMaxCornerRadius / 2 * multiplier } func layoutAvatarImmediately() { avatarHeight.active = true avatarWidth.active = true }Copy the code

AvatarHeight and avatarWidth over here are constraints on the width and height of the head that’s pulled from the Storyboard.

As for the effect of clicking on a menu item to display different data, at first glance it looks a bit like the multi-table view I wrote about earlier, but that idea doesn’t work very well here, because everything in the list (menu items, basic user information) has to be scrolled. If you follow that idea, We’re dealing with scrolling through two TableViews (or one ScrollView and one TableView) in the same dimension, which is unscientific.

So here I use only one TableView, using a different data source (UITableViewDataSource) when selecting different menu items:

lazy var userDynamicDataSource: UserDynamicDataSource = {
    let dataSource = UserDynamicDataSource()
    dataSource.userDynamicList = self.userDynamicList
    dataSource.name = self.userInfo.name
    dataSource.avatar = self.userInfo.avatar
    return dataSource
}()

lazy var topAnswerDataSource: TopAnswerDataSource = {
    let dataSource = TopAnswerDataSource()
    dataSource.topAnswerList = self.topAnswerList
    return dataSource
}()Copy the code

I put UI operations like changing color and moving indicator slider after clicking on menu items in UserMenu, and then delegate interaction with TableView to Controller:

weak var delegate: UserMenuDelegate? func addMenuItemTarget() { [dynamicButton, answerButton, moreButton].forEach { $0.addTarget(self, action: "selectMenuItem:", forControlEvents: .TouchUpInside) } } func selectMenuItem(item: UIButton) {// set the selected item to the selectedColor and restore the last selected item to the unselected color item.setTitleColor(selectedColor, forState: .Normal) lastSelectedItem.setTitleColor(deselectedColor, forState: .normal) lastSelectedItem = item let newCenterX = NSLayoutConstraint(item: indicator, attribute: .CenterX, relatedBy: .Equal, toItem: item, attribute: .CenterX, multiplier: 1, constant: 0) IndicatorCenterx. active = false indicatorCenterX = newCenterX IndicatorCenterx. active = true // Notify agent (via tag Initialize the corresponding menu type) delegate? .selectMenuItem(UserMenuItem(rawValue: item.tag)!) }Copy the code

UserMenuItem is an enum that represents the type of a menu item. Its rawValue corresponds to the tag of a Button and the rowHeight of a list:

Enum UserMenuItem: Int {// rawValue rowHeight case Dynamic = 100 case Answer = 80 case More = 0}Copy the code

This UserMenuDelegate is a delegate protocol defined by itself:

protocol UserMenuDelegate: class {
    func selectMenuItem(item: UserMenuItem)
}Copy the code

The Controller implements this protocol, so it can know which menu item is clicked, so it can configure the corresponding data source for the TableView. RowHeight can be obtained directly through rawValue:

// MARK: - UserMenuDelegate extension UserDetailViewController: UserMenuDelegate { func selectMenuItem(item: UserMenuItem) { guard userInfo ! = nil else { return } switch item { case .Dynamic: tableView.dataSource = userDynamicDataSource tableView.separatorStyle = .None case .Answer: tableView.dataSource = topAnswerDataSource tableView.separatorStyle = .SingleLine case .More: RowHeight = CGFloat(item.rawValue) tableView.reloadData()}} // Get rowHeight tableView.rowheight = CGFloat(item.rawValue) tableView.reloadData()}Copy the code

Also talk about MVC and MVVM

MVC is a very classic concept. It originated in SmallTalk. Gang of Four design Patterns introduced MVC in its introduction — separating Model and View through subscription/notification protocol. View uses instances of the Controller subclass to implement a specific response policy. Obviously, MVC in SmallTalk is view-centric. Both the Model and Controller could have been part of the View, but now the data part is separated into the Model and the logic processing the response is separated into the Controller. Does this feel like nothing you know about MVC? Because somewhere along the way, someone thought MVC should be mediated by the Controller between the Model and View, and the Model and View can’t talk to each other. So Controller becomes the center of MVC, and this idea is also the mainstream idea in iOS development. In the Stanford iOS open class, the old man with white beard showed a diagram explaining MVC:




Mainstream MVC. PNG

As you can see from this picture, the Controller has too much work to do. If the UI is written by hand, there is a lot of layout-related code in the Controller, which is very difficult to maintain. In 2005, Microsoft proposed the MVVM mode for the design of WPF. The main idea is to deal with user operations by responding to events based on bidirectional binding of Model and View data. So someone came up with the idea of using MVVM in iOS, but Cocoa Touch is different from WPF, so most of the time MVVM in iOS is actually M-VM-V-C, Add a ViewModel between the View and Model to handle data binding. The main purpose is to take some pressure off the Controller.

I think one of the main contradictions in iOS development, architecturally speaking, is that Controller is too much of a burden. So we don’t have to get hung up on all sorts of things, just think about what our Controller is doing so far:

  • The UI layout
  • Coordinate views
  • Coordinate View and Model
  • Handling the View’s response…

Let’s see what can be separated from Controller:

  • UI layout can be done in Storyboard or Xib, and if you want to do it in pure code it’s also best to use subclasses to customize the look and feel of a view, and to combine views, wrap them in a SUBclass of UIView, Don’t put a bunch of labels and buttons and addSubViews in the Controller.
  • Data binding between View and Model, you can set a method with Model as parameter in View, as long as the Controller call this method, the specific binding logic written in the View.
  • If you have only one data source for your TableView, you can have the Controller act as the data source. If you have multiple data sources, you can define them separately and then combine them into the Controller.
  • View responses, if they are UI-specific, such as changing color positions and sizes, can be handled by the View itself, but data related, or need to be coordinated with other views, can be handled by the Controller through the proxy.

I’ll use the code in the Kanzhihu project as an example to illustrate my own preferred approach. First, the UI layout is all done in Storyboard, so without the layout code, the View is empty. Then define a ViewModelType protocol:

protocol ViewModelType {
    typealias ModelType
    func bindModel(model: ModelType)
}Copy the code

Swift does not have a generic protocol, so it cannot write protocol ViewModelType directly, but it can also achieve the generic protocol effect by using typeAlias to define the parameter type.

Next, we have a TopAnswerCell, which has been laid out in Storyboard, pulling outlets to the View we want to use into the code, and then implementing the ViewModelType protocol:

class TopAnswerCell: UITableViewCell, ViewModelType {

    @IBOutlet weak var titleLabel: UILabel!

    @IBOutlet weak var agreeLabel: UILabel!

    @IBOutlet weak var dateLabel: UILabel!

    func bindModel(model: TopAnswerModel) {
        titleLabel.text = model.title
        agreeLabel.text = "\(model.agree)"
        dateLabel.text = model.date
    }
}Copy the code

So in the TableViewDataSource we just call bindModel directly:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(CellReuseIdentifier.User) as! TopUserCell
    let index = indexPath.row
    cell.bindModel((cellModelList[index], index))

    return cell
}Copy the code

The above is the example of processing Model and View. As for the example of processing response, I have mentioned it before, which is the example of imitating the UserMenu used in the user page of The Simple book. After clicking the menu item, the operation of changing color indicator sliding is completed inside the UserMenu. The part that’s going to interact with the TableView is going to be in the Controller. The case of multiple data sources was also mentioned above, and clicking on different menu items will use different data sources.

About protocol oriented programming

After Swift2, you can use Extension to add a default implementation to protocol methods or properties. This allows Swift to use the protocol to emulate the mixin effect that Ruby implements with Module, that is, to extend the functionality of a class through the protocol. For example, I have a custom RefreshControl:

class SimpleRefreshControl: UIRefreshControl { typealias Action = () -> () var action: Action! init(action: Action) { super.init() self.action = action self.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged) } func refresh() { self.action() delay(seconds: 1) { self.endRefreshing() } } required init? (coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }Copy the code

Its constructor takes a closure, which is called when it is refreshed, and then it finishes one second later.

Let me define another protocol:

protocol Refreshable: class { func getData() var simpleRefreshControl: SimpleRefreshControl { get } } extension Refreshable { var simpleRefreshControl: SimpleRefreshControl { return SimpleRefreshControl { [weak self] in self? .getData() } } }Copy the code

So if I have a bunch of TableView Controllers that all implement refresh, as long as they all implement Refreshable, and then define their own getData methods, And then in ViewDidLoad we can just add refreshControl equals simpleRefreshControl. If you do not use this protocol, you will have to write the following code many times

SimpleRefreshControl { [weak self] in self? .getData() }Copy the code

This example doesn’t have much code, so it may not be very effective. Using this technique, however, can make your code much leaner, more flexible, and more readable.

JSON Mapper

I have implemented a simple JSON-Model Mapper by myself, which is not perfect and is not recommended to be used in formal projects. Interested students can have a look at the ideas.

The last

There’s more to say, but it’s too long, and it’s too late, so I’ll leave it to you to look at the code.

Download the complete project source code

Feel useful if the trouble Star a ~ have a question welcome message exchange ^ ^