-
Notifications
You must be signed in to change notification settings - Fork 326
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #628 from yuta24/impl-announcements
[iOS] Implement announcements
- Loading branch information
Showing
30 changed files
with
589 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
ios-base/DroidKaigi 2020/Announcements/Models/AnnouncementsDataProvider.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import ios_combined | ||
import RxSwift | ||
|
||
protocol AnnouncementsDataProviderProtocol { | ||
func fetch() -> Single<[Announcement]> | ||
} | ||
|
||
final class AnnouncementsDataProvider: AnnouncementsDataProviderProtocol { | ||
enum Transformer { | ||
static let dateFormatter: DateFormatter = { | ||
let formatter = DateFormatter() | ||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXX" | ||
return formatter | ||
}() | ||
|
||
static func transform(_ response: String) -> Announcement.Type_? { | ||
switch response { | ||
case "NOTIFICATION": | ||
return .notification | ||
case "ALERT": | ||
return .alert | ||
case "FEEDBACK": | ||
return .feedback | ||
default: | ||
return .none | ||
} | ||
} | ||
|
||
static func transform(_ response: AnnouncementResponse) -> Announcement? { | ||
guard let publishedAt = dateFormatter.date(from: response.publishedAt)?.timeIntervalSince1970 else { | ||
return .none | ||
} | ||
guard let type = transform(response.type) else { | ||
return .none | ||
} | ||
|
||
return Announcement( | ||
id: response.id_, | ||
title: response.title, | ||
content: response.content, | ||
publishedAt: publishedAt, | ||
type: type | ||
) | ||
} | ||
} | ||
|
||
func fetch() -> Single<[Announcement]> { | ||
Single.create { observer in | ||
ApiComponentKt.generateDroidKaigiApi().getAnnouncements( | ||
lang: LangParameter.from(LangKt.defaultLang()), | ||
callback: { response in | ||
let response = response.compactMap(Transformer.transform).sorted { $0.publishedAt >= $1.publishedAt } | ||
observer(.success(response)) | ||
}, | ||
onError: { exception in | ||
observer(.error(KotlinError(localizedDescription: exception.description()))) | ||
} | ||
) | ||
|
||
return Disposables.create() | ||
} | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
ios-base/DroidKaigi 2020/Announcements/ViewModels/AnnouncementsViewModel.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import Foundation | ||
import ios_combined | ||
import RxCocoa | ||
import RxSwift | ||
|
||
protocol AnnouncementsViewModelType { | ||
// Output | ||
var announcements: Driver<[Announcement]> { get } | ||
var error: Driver<KotlinError?> { get } | ||
|
||
// Input | ||
func viewDidLoad() | ||
func pullToRefresh() | ||
} | ||
|
||
final class AnnouncementsViewModel: AnnouncementsViewModelType { | ||
let announcements: Driver<[Announcement]> | ||
let error: Driver<KotlinError?> | ||
|
||
private let viewDidLoadRelay = PublishRelay<Void>() | ||
private let pullToRefreshRelay = PublishRelay<Void>() | ||
|
||
private let disposeBag = DisposeBag() | ||
|
||
init( | ||
provider: AnnouncementsDataProviderProtocol | ||
) { | ||
let announcementsRelay = BehaviorRelay<[Announcement]>(value: []) | ||
let errorRelay = BehaviorRelay<KotlinError?>(value: nil) | ||
|
||
announcements = announcementsRelay.asDriver() | ||
error = errorRelay.asDriver() | ||
|
||
let fetchResult = Observable.merge(viewDidLoadRelay.asObservable(), pullToRefreshRelay.asObservable()) | ||
.flatMap { provider.fetch().asObservable().materialize() } | ||
.share() | ||
|
||
fetchResult.compactMap { $0.element } | ||
.bind(to: announcementsRelay) | ||
.disposed(by: disposeBag) | ||
fetchResult.map { $0.error as? KotlinError } | ||
.bind(to: errorRelay) | ||
.disposed(by: disposeBag) | ||
} | ||
|
||
func viewDidLoad() { | ||
viewDidLoadRelay.accept(()) | ||
} | ||
|
||
func pullToRefresh() { | ||
pullToRefreshRelay.accept(()) | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
ios-base/DroidKaigi 2020/Announcements/Views/AnnouncementsDataSource.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import ios_combined | ||
import RxCocoa | ||
import RxSwift | ||
import UIKit | ||
|
||
final class AnnouncementsDataSource: NSObject, RxTableViewDataSourceType, UITableViewDataSource { | ||
typealias Element = [Announcement] | ||
|
||
private var items = [Announcement]() | ||
|
||
func tableView(_ tableView: UITableView, observedEvent: Event<[Announcement]>) { | ||
Binder(self) { target, items in | ||
target.items = items | ||
tableView.reloadData() | ||
} | ||
.on(observedEvent) | ||
} | ||
|
||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | ||
return items.count | ||
} | ||
|
||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | ||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AnnouncementCell.self), for: indexPath) as! AnnouncementCell // swiftlint:disable:this force_cast | ||
cell.configure(items[indexPath.item]) | ||
return cell | ||
} | ||
} |
89 changes: 89 additions & 0 deletions
89
ios-base/DroidKaigi 2020/Announcements/Views/AnnouncementsViewController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import ios_combined | ||
import RxCocoa | ||
import RxSwift | ||
import UIKit | ||
|
||
final class AnnouncementsViewController: UIViewController { | ||
@IBOutlet weak var tableView: UITableView! { | ||
didSet { | ||
tableView.tableFooterView = UIView() | ||
tableView.separatorStyle = .none | ||
tableView.allowsSelection = false | ||
tableView.refreshControl = refreshControl | ||
tableView.register(UINib(nibName: String(describing: AnnouncementCell.self), bundle: .none), forCellReuseIdentifier: String(describing: AnnouncementCell.self)) | ||
} | ||
} | ||
|
||
override var preferredStatusBarStyle: UIStatusBarStyle { | ||
return .lightContent | ||
} | ||
|
||
private let refreshControl = UIRefreshControl() | ||
private let viewModel: AnnouncementsViewModelType | ||
private let disposeBag = DisposeBag() | ||
|
||
required init?(coder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
init(viewModel: AnnouncementsViewModelType) { | ||
self.viewModel = viewModel | ||
|
||
super.init(nibName: nil, bundle: nil) | ||
} | ||
|
||
override func viewDidLoad() { | ||
super.viewDidLoad() | ||
|
||
navigationController?.navigationBar.titleTextAttributes = [.foregroundColor: UIColor.black] | ||
navigationController?.navigationBar.barTintColor = ApplicationScheme.shared.colorScheme.backgroundColor | ||
navigationController?.navigationBar.tintColor = ApplicationScheme.shared.colorScheme.onBackgroundColor | ||
|
||
let templateMenuImage = Asset.icMenu.image.withRenderingMode(.alwaysTemplate) | ||
let menuItem = UIBarButtonItem(image: templateMenuImage, | ||
style: .plain, | ||
target: self, | ||
action: nil) | ||
let titleItem = UIBarButtonItem(title: L10n.announcements, | ||
style: .plain, | ||
target: nil, | ||
action: nil) | ||
titleItem.setTitleTextAttributes([.foregroundColor: UIColor.black], for: .disabled) | ||
titleItem.isEnabled = false | ||
navigationItem.leftBarButtonItems = [menuItem, titleItem] | ||
|
||
menuItem.rx.tap | ||
.bind(to: Binder(self) { target, _ in | ||
target.navigationDrawerController?.toggleLeftView() | ||
}) | ||
.disposed(by: disposeBag) | ||
|
||
let dataSource = AnnouncementsDataSource() | ||
|
||
refreshControl.rx.controlEvent(.valueChanged) | ||
.bind(to: Binder(self) { target, _ in | ||
target.refreshControl.beginRefreshing() | ||
target.viewModel.pullToRefresh() | ||
}) | ||
.disposed(by: disposeBag) | ||
|
||
viewModel.announcements | ||
.map { _ in } | ||
.drive(Binder(self) { target, _ in | ||
target.refreshControl.endRefreshing() | ||
}) | ||
.disposed(by: disposeBag) | ||
|
||
viewModel.announcements | ||
.drive(tableView.rx.items(dataSource: dataSource)) | ||
.disposed(by: disposeBag) | ||
|
||
viewModel.viewDidLoad() | ||
} | ||
} | ||
|
||
extension AnnouncementsViewController { | ||
static func instantiate() -> AnnouncementsViewController { | ||
AnnouncementsViewController(viewModel: AnnouncementsViewModel(provider: AnnouncementsDataProvider())) | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
ios-base/DroidKaigi 2020/Announcements/Views/AnnouncementsViewController.xib
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> | ||
<device id="retina6_1" orientation="portrait" appearance="light"/> | ||
<dependencies> | ||
<deployment identifier="iOS"/> | ||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/> | ||
<capability name="Safe area layout guides" minToolsVersion="9.0"/> | ||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||
</dependencies> | ||
<objects> | ||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AnnouncementsViewController" customModule="DroidKaigi_2020" customModuleProvider="target"> | ||
<connections> | ||
<outlet property="tableView" destination="WIp-LM-x6u" id="CfU-t5-jUe"/> | ||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/> | ||
</connections> | ||
</placeholder> | ||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> | ||
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT"> | ||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/> | ||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> | ||
<subviews> | ||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="WIp-LM-x6u"> | ||
<rect key="frame" x="0.0" y="44" width="414" height="818"/> | ||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> | ||
</tableView> | ||
</subviews> | ||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> | ||
<constraints> | ||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="WIp-LM-x6u" secondAttribute="trailing" id="G9U-Ah-osY"/> | ||
<constraint firstItem="WIp-LM-x6u" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" constant="20" symbolic="YES" id="YVX-7K-KLE"/> | ||
<constraint firstItem="fnl-2z-Ty3" firstAttribute="bottom" secondItem="WIp-LM-x6u" secondAttribute="bottom" id="lZL-21-9I6"/> | ||
<constraint firstItem="WIp-LM-x6u" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="tUo-OX-nt5"/> | ||
</constraints> | ||
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/> | ||
<point key="canvasLocation" x="139" y="99"/> | ||
</view> | ||
</objects> | ||
</document> |
60 changes: 60 additions & 0 deletions
60
ios-base/DroidKaigi 2020/Announcements/Views/Cells/AnnouncementCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import ios_combined | ||
import UIKit | ||
|
||
private extension Announcement.Type_ { | ||
var iconImage: UIImage? { | ||
switch self { | ||
case .alert: | ||
return #imageLiteral(resourceName: "warning_amber_24px") | ||
case .feedback: | ||
return #imageLiteral(resourceName: "assignment_24px") | ||
case .notification: | ||
return #imageLiteral(resourceName: "error_outline_24px") | ||
default: | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
final class AnnouncementCell: UITableViewCell { | ||
enum Constant { | ||
static let dateFormatter: DateFormatter = { | ||
let formatter = DateFormatter() | ||
formatter.locale = Locale.current | ||
formatter.calendar = Calendar.current | ||
formatter.dateFormat = DateFormatter.dateFormat(fromTemplate: "dMMM HH:mm", options: 0, locale: Locale.current) | ||
return formatter | ||
}() | ||
} | ||
|
||
@IBOutlet weak var iconImageContainerView: UIView! { | ||
didSet { | ||
iconImageContainerView.clipsToBounds = true | ||
iconImageContainerView.backgroundColor = Asset.secondary50.color | ||
iconImageContainerView.layer.cornerRadius = 16 | ||
} | ||
} | ||
|
||
@IBOutlet weak var iconImageView: UIImageView! | ||
@IBOutlet weak var publishedAtLabel: UILabel! | ||
@IBOutlet weak var titleLabel: UILabel! | ||
@IBOutlet weak var contentTextView: UITextView! | ||
|
||
func configure(_ announcement: Announcement) { | ||
iconImageView.image = announcement.type.iconImage?.withRenderingMode(.alwaysOriginal).tint(with: Asset.secondary300.color) | ||
publishedAtLabel.text = Constant.dateFormatter.string(from: Date(timeIntervalSince1970: announcement.publishedAt)) | ||
titleLabel.text = announcement.title | ||
contentTextView.attributedText = { content in | ||
guard | ||
let data = content.data(using: .unicode), | ||
let attributedString = try? NSMutableAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) | ||
else { | ||
return nil | ||
} | ||
|
||
let range = NSRange(location: 0, length: attributedString.length) | ||
attributedString.addAttribute(.font, value: UIFont.preferredFont(forTextStyle: .body), range: range) | ||
return attributedString | ||
}(announcement.content) | ||
} | ||
} |
Oops, something went wrong.