Skip to content

Commit 9ec13d1

Browse files
committed
Example: main screen with folders list
1 parent 0db3a8a commit 9ec13d1

18 files changed

+558
-51
lines changed

Example/AppDelegate.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2626
print("AutoLogin result: \(result)")
2727
}
2828

29+
// Prepare for UI
30+
MainViewController.prepareForMake()
31+
2932
return true
3033
}
3134

Example/Common/Observable.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// Observable.swift
3+
// Example
4+
//
5+
// Created by Виталий Короткий on 02.01.2021.
6+
// Copyright © 2021 ProVir. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import Combine
11+
12+
@propertyWrapper
13+
final class Observable<T> {
14+
private let subject: CurrentValueSubject<T, Never>
15+
16+
init(wrappedValue: T) {
17+
self.wrappedValue = wrappedValue
18+
self.subject = .init(wrappedValue)
19+
}
20+
21+
func resendCurrentValue() {
22+
subject.send(wrappedValue)
23+
}
24+
25+
var wrappedValue: T {
26+
didSet {
27+
subject.value = wrappedValue
28+
}
29+
}
30+
31+
var projectedValue: AnyPublisher<T, Never> { subject.eraseToAnyPublisher() }
32+
}
Lines changed: 85 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,96 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V7P-K7-Adh">
3+
<device id="retina6_1" orientation="portrait" appearance="light"/>
34
<dependencies>
4-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
5-
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
5+
<deployment identifier="iOS"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
7+
<capability name="System colors in document resources" minToolsVersion="11.0"/>
68
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
79
</dependencies>
810
<scenes>
9-
<!--View Controller-->
10-
<scene sceneID="tne-QT-ifu">
11+
<!--Folders-->
12+
<scene sceneID="BvB-HF-OT2">
1113
<objects>
12-
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
13-
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
14-
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
14+
<tableViewController id="uQv-5d-hd3" customClass="MainViewController" customModule="Example" customModuleProvider="target" sceneMemberID="viewController">
15+
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="xCx-Qe-zML">
16+
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
1517
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
16-
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
17-
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
18-
</view>
19-
</viewController>
20-
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
18+
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
19+
<prototypes>
20+
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="cell" textLabel="wfc-YG-O31" detailTextLabel="iNE-6N-UKh" style="IBUITableViewCellStyleSubtitle" id="enq-1H-5uW">
21+
<rect key="frame" x="0.0" y="28" width="414" height="55.5"/>
22+
<autoresizingMask key="autoresizingMask"/>
23+
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="enq-1H-5uW" id="Mdn-aL-YBD">
24+
<rect key="frame" x="0.0" y="0.0" width="383" height="55.5"/>
25+
<autoresizingMask key="autoresizingMask"/>
26+
<subviews>
27+
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wfc-YG-O31">
28+
<rect key="frame" x="20" y="10" width="33" height="20.5"/>
29+
<autoresizingMask key="autoresizingMask"/>
30+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
31+
<nil key="textColor"/>
32+
<nil key="highlightedColor"/>
33+
</label>
34+
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="iNE-6N-UKh">
35+
<rect key="frame" x="20" y="31.5" width="44" height="14.5"/>
36+
<autoresizingMask key="autoresizingMask"/>
37+
<fontDescription key="fontDescription" type="system" pointSize="12"/>
38+
<nil key="textColor"/>
39+
<nil key="highlightedColor"/>
40+
</label>
41+
</subviews>
42+
</tableViewCellContentView>
43+
</tableViewCell>
44+
</prototypes>
45+
<connections>
46+
<outlet property="dataSource" destination="uQv-5d-hd3" id="sar-bf-ONg"/>
47+
<outlet property="delegate" destination="uQv-5d-hd3" id="cca-0t-TDV"/>
48+
</connections>
49+
</tableView>
50+
<navigationItem key="navigationItem" title="Folders" id="xV0-JN-ufi">
51+
<barButtonItem key="leftBarButtonItem" image="person.circle" catalog="system" id="lYO-hn-aKN">
52+
<connections>
53+
<action selector="manageUser" destination="uQv-5d-hd3" id="FQK-KE-BTn"/>
54+
</connections>
55+
</barButtonItem>
56+
<barButtonItem key="rightBarButtonItem" title="Item" systemItem="add" id="cDS-TO-5Ne">
57+
<connections>
58+
<action selector="addFolder" destination="uQv-5d-hd3" id="0n7-PC-Kh1"/>
59+
</connections>
60+
</barButtonItem>
61+
</navigationItem>
62+
<refreshControl key="refreshControl" opaque="NO" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="eDL-vY-AnW">
63+
<autoresizingMask key="autoresizingMask"/>
64+
<connections>
65+
<action selector="refresh" destination="uQv-5d-hd3" eventType="valueChanged" id="DoN-M1-d2c"/>
66+
</connections>
67+
</refreshControl>
68+
</tableViewController>
69+
<placeholder placeholderIdentifier="IBFirstResponder" id="JuM-GR-pYU" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
2170
</objects>
71+
<point key="canvasLocation" x="394" y="730"/>
72+
</scene>
73+
<!--Navigation Controller-->
74+
<scene sceneID="atc-EO-pgX">
75+
<objects>
76+
<navigationController id="V7P-K7-Adh" sceneMemberID="viewController">
77+
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="d8z-0B-sOV">
78+
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
79+
<autoresizingMask key="autoresizingMask"/>
80+
</navigationBar>
81+
<connections>
82+
<segue destination="uQv-5d-hd3" kind="relationship" relationship="rootViewController" id="dUb-hO-hku"/>
83+
</connections>
84+
</navigationController>
85+
<placeholder placeholderIdentifier="IBFirstResponder" id="FBd-ru-QKo" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
86+
</objects>
87+
<point key="canvasLocation" x="-499" y="730"/>
2288
</scene>
2389
</scenes>
90+
<resources>
91+
<image name="person.circle" catalog="system" width="128" height="121"/>
92+
<systemColor name="systemBackgroundColor">
93+
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
94+
</systemColor>
95+
</resources>
2496
</document>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
//
2+
// MainPresenter.swift
3+
// Example
4+
//
5+
// Created by Виталий Короткий on 02.01.2021.
6+
// Copyright © 2021 ProVir. All rights reserved.
7+
//
8+
9+
import Foundation
10+
import Combine
11+
import ServiceContainerKit
12+
13+
protocol MainPresenter: class {
14+
func configure(
15+
showAlertHandler: @escaping (String, String) -> Void,
16+
routeToFolderHandler: @escaping (NoteFolder) -> Void
17+
)
18+
19+
var titlePublisher: AnyPublisher<String, Never> { get }
20+
var modelsPublisher: AnyPublisher<[SimpleCellViewModel], Never> { get }
21+
var isAuthUser: Bool { get }
22+
23+
func login(user: String)
24+
func logoutUser()
25+
26+
func addFolder(name: String)
27+
func reload(completion: @escaping () -> Void)
28+
}
29+
30+
final class MainPresenterImpl: MainPresenter {
31+
@ServiceInject(\Services.user.userService)
32+
private var userService
33+
34+
@ServiceInject(\Services.folders.manager)
35+
private var foldersManager
36+
37+
private var showAlert: (String, String) -> Void = { _, _ in }
38+
private var routeToFolder: (NoteFolder) -> Void = { _ in }
39+
40+
private var cancellableSet: Set<AnyCancellable> = []
41+
42+
var titlePublisher: AnyPublisher<String, Never> {
43+
userService.userPublisher.map {
44+
if let user = $0 {
45+
return "Folders for \"\(user.login)\""
46+
} else {
47+
return "Not authorized user"
48+
}
49+
}.eraseToAnyPublisher()
50+
}
51+
52+
@Observable
53+
private var models: [SimpleCellViewModel] = []
54+
var modelsPublisher: AnyPublisher<[SimpleCellViewModel], Never> { $models }
55+
56+
var isAuthUser: Bool { userService.user != nil }
57+
58+
func configure(
59+
showAlertHandler: @escaping (String, String) -> Void,
60+
routeToFolderHandler: @escaping (NoteFolder) -> Void
61+
) {
62+
self.showAlert = showAlertHandler
63+
self.routeToFolder = routeToFolderHandler
64+
65+
foldersManager.foldersPublisher.map { [weak self] in
66+
guard let self = self else { return [] }
67+
return $0.map { self.buildCell(folder: $0) }
68+
}.assign(to: \Self.models, on: self)
69+
.store(in: &cancellableSet)
70+
}
71+
72+
func login(user: String) {
73+
userService.auth(login: user) { [weak self] in
74+
self?.handleOperation(result: $0, errorTitle: "Failed change user")
75+
}
76+
}
77+
78+
func logoutUser() {
79+
userService.logout()
80+
}
81+
82+
func addFolder(name: String) {
83+
foldersManager.add(content: .init(name: name)) { [weak self] in
84+
self?.handleOperation(result: $0, errorTitle: "Failed to create folder")
85+
}
86+
}
87+
88+
func reload(completion: @escaping () -> Void) {
89+
foldersManager.reload { [weak self] in
90+
self?.handleOperation(result: $0, errorTitle: "Failed refresh list folders")
91+
completion()
92+
}
93+
}
94+
95+
// MARK: - Private
96+
private func handleOperation<T>(result: Result<T, Error>, errorTitle: @autoclosure () -> String) {
97+
switch result {
98+
case .success: break
99+
case .failure(let error):
100+
showErrorAlert(title: errorTitle(), error: error)
101+
}
102+
}
103+
104+
private func showErrorAlert(title: String, error: Error) {
105+
let message = error.localizedDescription
106+
showAlert(title, message)
107+
}
108+
109+
private func deleteFolder(folder: NoteFolder) {
110+
foldersManager.remove(folderId: folder.id) { [weak self] result in
111+
switch result {
112+
case .success: break
113+
case .failure(let error):
114+
self?.showErrorAlert(title: "Failed to remove folder", error: error)
115+
self?._models.resendCurrentValue()
116+
}
117+
}
118+
}
119+
120+
// MARK: Builders
121+
private func buildCell(folder: NoteFolder) -> SimpleCellViewModel {
122+
return .init(
123+
text: folder.content.name,
124+
detail: nil,
125+
onSelected: { [weak self] in self?.routeToFolder(folder) },
126+
onDeleted: { [weak self] in self?.deleteFolder(folder: folder) }
127+
)
128+
}
129+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
//
2+
// MainViewController.swift
3+
// Example
4+
//
5+
// Created by Виталий Короткий on 02.01.2021.
6+
// Copyright © 2021 ProVir. All rights reserved.
7+
//
8+
9+
import UIKit
10+
import Combine
11+
import ServiceContainerKit
12+
13+
extension MainViewController {
14+
static func prepareForMake() {
15+
let presenter = MainPresenterImpl()
16+
EntityInjectResolver.registerForFirstInject(presenter)
17+
}
18+
}
19+
20+
class MainViewController: SimpleTableViewController {
21+
@EntityInject(MainPresenter.self) var presenter
22+
23+
var cancellableSet: Set<AnyCancellable> = []
24+
25+
override func viewDidLoad() {
26+
super.viewDidLoad()
27+
28+
presenter.configure(
29+
showAlertHandler: { [weak self] title, message in
30+
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
31+
alert.addAction(.init(title: "OK", style: .default, handler: nil))
32+
self?.present(alert, animated: true, completion: nil)
33+
},
34+
routeToFolderHandler: { folder in
35+
print("Routed to \(folder.id)")
36+
}
37+
)
38+
presenter.titlePublisher.sink { [weak self] in
39+
self?.navigationItem.title = $0
40+
}.store(in: &cancellableSet)
41+
presenter.modelsPublisher.sink { [weak self] in
42+
self?.adapter.update(models: $0)
43+
}.store(in: &cancellableSet)
44+
}
45+
46+
// MARK: Actions
47+
@IBAction private func refresh() {
48+
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
49+
self?.presenter.reload {
50+
self?.refreshControl?.endRefreshing()
51+
}
52+
}
53+
}
54+
55+
@IBAction private func manageUser() {
56+
let isAuthUser = presenter.isAuthUser
57+
let message = isAuthUser ? "Change user or logout?" : "Login user"
58+
let alert = UIAlertController(title: "User manager", message: message, preferredStyle: .alert)
59+
60+
var textField: UITextField?
61+
alert.addTextField {
62+
$0.placeholder = "Login"
63+
textField = $0
64+
}
65+
66+
alert.addAction(.init(title: "Cancel", style: .cancel, handler: nil))
67+
alert.addAction(.init(title: "Login", style: .default) { [weak self] _ in
68+
guard let login = textField?.text else { return }
69+
self?.presenter.login(user: login)
70+
})
71+
72+
if isAuthUser {
73+
alert.addAction(.init(title: "Logout", style: .destructive) { [weak self] _ in
74+
self?.presenter.logoutUser()
75+
})
76+
}
77+
78+
present(alert, animated: true, completion: nil)
79+
}
80+
81+
@IBAction private func addFolder() {
82+
let alert = UIAlertController(title: "New folder", message: "Enter the name of the folder", preferredStyle: .alert)
83+
84+
var textField: UITextField?
85+
alert.addTextField {
86+
$0.placeholder = "Name folder"
87+
textField = $0
88+
}
89+
90+
alert.addAction(.init(title: "Cancel", style: .cancel, handler: nil))
91+
alert.addAction(.init(title: "Create", style: .default) { [weak self] _ in
92+
guard let name = textField?.text, name.isEmpty == false else { return }
93+
self?.presenter.addFolder(name: name)
94+
})
95+
96+
present(alert, animated: true, completion: nil)
97+
}
98+
99+
/*
100+
// MARK: - Navigation
101+
102+
// In a storyboard-based application, you will often want to do a little preparation before navigation
103+
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
104+
// Get the new view controller using segue.destination.
105+
// Pass the selected object to the new view controller.
106+
}
107+
*/
108+
109+
}

0 commit comments

Comments
 (0)