People menu in IGListKit+Swift (#1851)

* Create new people with IGLK+Swift, fix AX+l18n, fix bug where cant remove people

* smaller font for people cell
This commit is contained in:
Ryan Nystrom
2018-06-03 18:10:46 -04:00
committed by GitHub
parent 87882cbc30
commit c97598be84
8 changed files with 239 additions and 19 deletions

View File

@@ -9,7 +9,7 @@
import Foundation
import IGListKit
final class IssueAssigneeViewModel: ListDiffable {
final class IssueAssigneeViewModel: ListDiffable, ListSwiftIdentifiable, ListSwiftEquatable {
let login: String
let avatarURL: URL
@@ -28,4 +28,16 @@ final class IssueAssigneeViewModel: ListDiffable {
return true
}
// MARK: ListSwiftIdentifiable
var identifier: String {
return avatarURL.absoluteString
}
// MARK: ListSwiftEquatable
func isEqual(to object: ListSwiftDiffable) -> Bool {
return true
}
}

View File

@@ -511,7 +511,7 @@ extension GithubClient {
number: number,
type: type,
add: false,
people: added)
people: removed)
) { result in
switch result {
case .success: break

View File

@@ -107,13 +107,13 @@ ContextMenuDelegate {
)
}
func newPeopleController(type: PeopleViewController.PeopleType) -> UIViewController {
func newPeopleController(type: PeopleViewController2.PeopleType) -> UIViewController {
let selections: [String]
switch type {
case .assignee: selections = issueResult?.assignee.users.map { $0.login } ?? []
case .reviewer: selections = issueResult?.reviewers?.users.map { $0.login } ?? []
}
return PeopleViewController(
return PeopleViewController2(
selections: selections,
type: type,
client: client,
@@ -284,12 +284,8 @@ ContextMenuDelegate {
// MARK: PeopleViewControllerDelegate
func didDismiss(controller: PeopleViewController) {
func didDismiss(controller: PeopleViewController2) {
guard let previous = issueResult else { return }
var assignees = [IssueAssigneeViewModel]()
for user in controller.selectedUsers {
assignees.append(IssueAssigneeViewModel(login: user.login, avatarURL: user.avatarUrl))
}
let mutationType: V3AddPeopleRequest.PeopleType
switch controller.type {
@@ -303,7 +299,7 @@ ContextMenuDelegate {
owner: model.owner,
repo: model.repo,
number: model.number,
people: assignees
people: controller.selected
)
}
@@ -314,7 +310,7 @@ ContextMenuDelegate {
didDismiss(selected: labels.selected)
} else if let milestones = viewController as? MilestonesViewController2 {
didDismiss(controller: milestones)
} else if let people = viewController as? PeopleViewController {
} else if let people = viewController as? PeopleViewController2 {
didDismiss(controller: people)
} else if let labels = viewController as? LabelsViewController2 {
didDismiss(selected: labels.selected)

View File

@@ -10,14 +10,17 @@ import UIKit
import SDWebImage
import SnapKit
final class PeopleCell: UICollectionViewCell {
final class PeopleCell: SelectableCell {
let avatarImageView = UIImageView()
let usernameLabel = UILabel()
let checkmarkImageView = UIImageView()
private let avatarImageView = UIImageView()
private let usernameLabel = UILabel()
private let checkmarkImageView = UIImageView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
contentView.addSubview(avatarImageView)
contentView.addSubview(usernameLabel)
contentView.addSubview(checkmarkImageView)
@@ -28,12 +31,13 @@ final class PeopleCell: UICollectionViewCell {
make.size.equalTo(Styles.Sizes.avatar)
}
usernameLabel.font = Styles.Text.secondary.preferredFont
usernameLabel.snp.makeConstraints { make in
make.centerY.equalTo(self.contentView)
make.left.equalTo(avatarImageView.snp.right).offset(Styles.Sizes.gutter)
}
checkmarkImageView.image = UIImage(named: "check")?.withRenderingMode(.alwaysTemplate)
checkmarkImageView.image = UIImage(named: "check-small")?.withRenderingMode(.alwaysTemplate)
checkmarkImageView.tintColor = Styles.Colors.Blue.medium.color
checkmarkImageView.contentMode = .scaleAspectFit

View File

@@ -0,0 +1,50 @@
//
// PeopleSectionController2.swift
// Freetime
//
// Created by Ryan Nystrom on 6/2/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import IGListKit
protocol PeopleSectionController2Delegate: class {
func didSelect(controller: PeopleSectionController2)
}
final class PeopleSectionController2: ListSwiftSectionController<IssueAssigneeViewModel> {
public weak var delegate: PeopleSectionController2Delegate?
public private(set) var selected: Bool
init(selected: Bool) {
self.selected = selected
super.init()
}
override func createBinders(from value: IssueAssigneeViewModel) -> [ListBinder] {
return [
binder(
value,
cellType: ListCellType.class(PeopleCell.self),
size: {
return CGSize(
width: $0.collection.containerSize.width,
height: Styles.Sizes.tableCellHeight
)
},
configure: { [selected] in
$0.configure(avatarURL: $1.value.avatarURL, username: $1.value.login, showCheckmark: selected)
},
didSelect: { [weak self] context in
guard let strongSelf = self else { return }
strongSelf.selected = !strongSelf.selected
context.deselect()
context.cell?.setCellState(selected: strongSelf.selected)
strongSelf.delegate?.didSelect(controller: strongSelf)
})
]
}
}

View File

@@ -0,0 +1,139 @@
//
// PeopleViewController2.swift
// Freetime
//
// Created by Ryan Nystrom on 6/2/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
import IGListKit
import GitHubAPI
final class PeopleViewController2: BaseListViewController2,
BaseListViewController2DataSource,
PeopleSectionController2Delegate {
enum PeopleType {
case assignee
case reviewer
}
public let type: PeopleType
private let selections: Set<String>
private let selectionLimit = 10
private var users = [IssueAssigneeViewModel]()
private let client: GithubClient
private var owner: String
private var repo: String
init(
selections: [String],
type: PeopleType,
client: GithubClient,
owner: String,
repo: String
) {
self.selections = Set<String>(selections)
self.type = type
self.client = client
self.owner = owner
self.repo = repo
super.init(emptyErrorMessage: NSLocalizedString("Cannot load users.", comment: ""))
self.dataSource = self
switch type {
case .assignee: title = NSLocalizedString("Assignees", comment: "")
case .reviewer: title = NSLocalizedString("Reviewers", comment: "")
}
preferredContentSize = CGSize(width: 280, height: 240)
updateSelectionCount()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: Public API
var selected: [IssueAssigneeViewModel] {
return users.filter {
if let sectionController: PeopleSectionController2 = feed.swiftAdapter.sectionController(for: $0) {
return sectionController.selected
}
return false
}
}
// MARK: Private API
func updateSelectionCount() {
let label = UILabel()
label.font = Styles.Text.body.preferredFont
label.backgroundColor = .clear
label.textColor = Styles.Colors.Gray.light.color
label.text = "\(selected.count)/\(selectionLimit)"
label.sizeToFit()
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: label)
}
// MARK: Overrides
override func fetch(page: String?) {
client.client.send(
V3AssigneesRequest(
owner: owner,
repo: repo,
page: (page as NSString?)?.integerValue ?? 1
)
) { [weak self] result in
switch result {
case .success(let response):
let sortedUsers = response.data.sorted {
$0.login.caseInsensitiveCompare($1.login) == .orderedAscending
}
let users = sortedUsers.map { IssueAssigneeViewModel(login: $0.login, avatarURL: $0.avatarUrl) }
if page != nil {
self?.users += users
} else {
self?.users = users
}
self?.update(animated: true)
let nextPage: String?
if let next = response.next {
nextPage = "\(next)"
} else {
nextPage = nil
}
self?.update(page: nextPage, animated: true)
case .failure:
ToastManager.showGenericError()
}
}
}
// MARK: BaseListViewController2DataSource
func models(adapter: ListSwiftAdapter) -> [ListSwiftPair] {
return users.map { [selections] user in
return ListSwiftPair.pair(user) { [weak self] in
let controller = PeopleSectionController2(selected: selections.contains(user.login))
controller.delegate = self
return controller
}
}
}
// MARK: PeopleSectionController2Delegate
func didSelect(controller: PeopleSectionController2) {
updateSelectionCount()
}
}

View File

@@ -57,7 +57,10 @@ FeedDelegate {
// MARK: Public API
final func update(animated: Bool) {
self.feed.finishLoading(dismissRefresh: true, animated: animated)
self.feed.finishLoading(
dismissRefresh: true,
animated: animated && trueUnlessReduceMotionEnabled
)
}
final func update(
@@ -69,7 +72,11 @@ FeedDelegate {
self.hasError = false
self.page = page
self.feed.finishLoading(dismissRefresh: true, animated: animated, completion: completion)
self.feed.finishLoading(
dismissRefresh: true,
animated: animated && trueUnlessReduceMotionEnabled,
completion: completion
)
}
final func error(
@@ -78,7 +85,11 @@ FeedDelegate {
) {
assert(Thread.isMainThread)
hasError = true
feed.finishLoading(dismissRefresh: true, animated: animated, completion: completion)
feed.finishLoading(
dismissRefresh: true,
animated: animated && trueUnlessReduceMotionEnabled,
completion: completion
)
}
// MARK: FeedDelegate

View File

@@ -241,6 +241,8 @@
297EC91B20C4744800B6E42C /* MilestonesViewController2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297EC91A20C4744800B6E42C /* MilestonesViewController2.swift */; };
297EC91D20C4765000B6E42C /* MilestoneSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297EC91C20C4765000B6E42C /* MilestoneSectionController.swift */; };
297EC91F20C4768600B6E42C /* MilestoneViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297EC91E20C4768600B6E42C /* MilestoneViewModel.swift */; };
297EC91520C3953300B6E42C /* PeopleSectionController2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297EC91420C3953300B6E42C /* PeopleSectionController2.swift */; };
297EC91720C3965E00B6E42C /* PeopleViewController2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297EC91620C3965E00B6E42C /* PeopleViewController2.swift */; };
297FB7781F51128A00F2E618 /* SettingsAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 297FB7771F51128A00F2E618 /* SettingsAccountsViewController.swift */; };
2980033C1F51E82400BE90F4 /* RatingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2980033B1F51E82400BE90F4 /* RatingController.swift */; };
2980033E1F51E93500BE90F4 /* RatingSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2980033D1F51E93500BE90F4 /* RatingSectionController.swift */; };
@@ -750,6 +752,8 @@
297EC91A20C4744800B6E42C /* MilestonesViewController2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestonesViewController2.swift; sourceTree = "<group>"; };
297EC91C20C4765000B6E42C /* MilestoneSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneSectionController.swift; sourceTree = "<group>"; };
297EC91E20C4768600B6E42C /* MilestoneViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MilestoneViewModel.swift; sourceTree = "<group>"; };
297EC91420C3953300B6E42C /* PeopleSectionController2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleSectionController2.swift; sourceTree = "<group>"; };
297EC91620C3965E00B6E42C /* PeopleViewController2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleViewController2.swift; sourceTree = "<group>"; };
297FB7771F51128A00F2E618 /* SettingsAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountsViewController.swift; sourceTree = "<group>"; };
2980033B1F51E82400BE90F4 /* RatingController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RatingController.swift; sourceTree = "<group>"; };
2980033D1F51E93500BE90F4 /* RatingSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RatingSectionController.swift; sourceTree = "<group>"; };
@@ -1411,8 +1415,10 @@
isa = PBXGroup;
children = (
295B51411FC26B8100C3993B /* PeopleCell.swift */,
297EC91620C3965E00B6E42C /* PeopleViewController2.swift */,
295B51431FC26C5400C3993B /* PeopleViewController.swift */,
1F9D00F92051089C00DF5FF6 /* PeopleSectionController.swift */,
297EC91420C3953300B6E42C /* PeopleSectionController2.swift */,
);
path = People;
sourceTree = "<group>";
@@ -2890,6 +2896,7 @@
986B873E1F2E1CE400AAB55C /* RepositoryClient.swift in Sources */,
295A77BE1F75C1CC007BC403 /* RepositoryDetails.swift in Sources */,
292ACE1E1F5CB02400C9A02C /* RepositoryEmptyResultsCell.swift in Sources */,
297EC91720C3965E00B6E42C /* PeopleViewController2.swift in Sources */,
292ACE1F1F5CB02400C9A02C /* RepositoryEmptyResultsSectionController.swift in Sources */,
292ACE1B1F5CAFAD00C9A02C /* RepositoryEmptyResultsType.swift in Sources */,
29AF1E881F8AB0AA0008A0EF /* IssueTextActionsView+Markdown.swift in Sources */,
@@ -2938,6 +2945,7 @@
986B87251F2B990A00AAB55C /* SearchRepoResultCell.swift in Sources */,
986B87231F2B98AD00AAB55C /* SearchResultSectionController.swift in Sources */,
29AF1E8E1F8ABC900008A0EF /* RepositoryFile.swift in Sources */,
297EC91520C3953300B6E42C /* PeopleSectionController2.swift in Sources */,
986B871D1F2B8FCD00AAB55C /* SearchViewController.swift in Sources */,
298BA0971EC947F100B01946 /* SegmentedControlCell.swift in Sources */,
297B062C1FB92DCA0026FA23 /* UICollectionViewLayout+Orientation.swift in Sources */,