Last active
February 5, 2019 02:27
-
-
Save takehilo/a4d2785f7c09f192d9bf77ff3cbb9d21 to your computer and use it in GitHub Desktop.
iOS設計パターン入門 第3章 MessageSenderの実装例
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
import UIKit | |
import PlaygroundSupport | |
let page = PlaygroundPage.current | |
page.needsIndefiniteExecution = true | |
protocol Message {} | |
struct TextMessage: Message, Decodable { | |
var text: String? | |
} | |
struct ImageMessage: Message { | |
var text: String? | |
let image: UIImage? | |
} | |
enum ImageMessageInputError: Error { | |
case noImage | |
case tooLongText(count: Int) | |
} | |
protocol MessageInput { | |
associatedtype Payload | |
func validate() throws -> Payload | |
} | |
struct TextMessageInput: MessageInput { | |
var text: String? | |
func validate() throws -> TextMessageInput { | |
if let text = text, text.count >= 80 { | |
throw ImageMessageInputError.tooLongText(count: text.count) } | |
return TextMessageInput(text: text) | |
} | |
} | |
struct ImageMessageInput: MessageInput { | |
var text: String? | |
var image: UIImage? | |
func validate() throws -> ImageMessageInput { | |
guard let image = image else { throw ImageMessageInputError.noImage } | |
if let text = text, text.count >= 80 { | |
throw ImageMessageInputError.tooLongText(count: text.count) } | |
return ImageMessageInput(text: text, image: image) | |
} | |
} | |
protocol MessageSenderAPI { | |
associatedtype Payload | |
associatedtype Response: Message | |
func send(payload: Payload, completion: @escaping (Response?) -> Void) | |
} | |
struct TextMessageSenderAPI: MessageSenderAPI { | |
static let shared = TextMessageSenderAPI() | |
private init() {} | |
func send(payload: TextMessageInput, completion: @escaping (TextMessage?) -> Void) { | |
let url = URL(string: "https://api.example.com/send")! | |
var request = URLRequest(url: url) | |
request.httpMethod = "POST" | |
request.setValue("application/json", forHTTPHeaderField: "Content-Type") | |
request.httpBody = payload.text?.data(using: .utf8) | |
let task = URLSession.shared.dataTask(with: request) { data, response, error in | |
guard let data = data else { | |
completion(nil) | |
return | |
} | |
do { | |
let message = try JSONDecoder().decode(TextMessage.self, from: data) | |
completion(message) | |
} catch { | |
completion(nil) | |
} | |
} | |
task.resume() | |
} | |
} | |
protocol MessageSenderDelegate { | |
func stateChanged<API: MessageSenderAPI, Input: MessageInput>(_ state: MessageSender<API, Input>.State) | |
} | |
final class MessageSender<API: MessageSenderAPI, Input: MessageInput> where API.Payload == Input.Payload { | |
enum State { | |
case inputting(validationError: Error?) | |
case sending | |
case sent(API.Response) | |
case connectionFailed | |
init(evaluating input: Input) { | |
do { | |
_ = try input.validate() | |
self = .inputting(validationError: nil) | |
} catch { | |
self = .inputting(validationError: error) | |
} | |
} | |
mutating func accept(response: API.Response?) { | |
if let response = response { | |
self = .sent(response) | |
} else { | |
self = .connectionFailed | |
} | |
} | |
} | |
private(set) var state: State { | |
didSet { delegate?.stateChanged(state) } | |
} | |
let api: API | |
var input: Input { | |
didSet { state = State(evaluating: input) } | |
} | |
var delegate: MessageSenderDelegate? | |
init(api: API, input: Input) { | |
self.api = api | |
self.input = input | |
self.state = State(evaluating: input) | |
} | |
func send() { | |
do { | |
let payload = try input.validate() | |
state = .sending | |
api.send(payload: payload) { [weak self] in | |
self?.state.accept(response: $0) | |
} | |
} catch let e { | |
state = .inputting(validationError: e) | |
} | |
} | |
} | |
class Delegate: MessageSenderDelegate { | |
func stateChanged<API, Input>(_ state: MessageSender<API, Input>.State) { | |
switch state { | |
case .sending: | |
print("Sending...") | |
case .connectionFailed: | |
print("Failed to send") | |
case let .sent(message): | |
if let textMessage = message as? TextMessage { | |
print("Successfully sent the message (text: \(textMessage.text ?? ""))") | |
} else { | |
print("Successfully sent the message") | |
} | |
case let .inputting(error): | |
if let error = error { | |
print("Validation error: \(error.localizedDescription)") | |
} | |
} | |
} | |
} | |
let api = TextMessageSenderAPI.shared | |
let input = TextMessageInput(text: "Hello, world!") | |
let sender = MessageSender(api: api, input: input) | |
sender.delegate = Delegate() | |
sender.send() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Swift Playgroundに貼っ付ければそのまま動くはず。