Functional Swift 4章 "Map Filter Reduce" まとめ
引数に関数を取る関数を高階関数と呼びます。本章ではSwiftの標準ライブラリに実装されている高階関数を紹介します。
Map, Generic
Int型の配列を受け取り、全ての要素に1を加えて返す関数はfor文を使って簡単に書くことができます。
func incrementArray(xs: [Int]) -> [Int] { var result: [Int] = [] for x in xs { result.append(x + 1) } return result }
全ての要素を2倍にする関数も同様に書くことができるでしょう。
しかし、これらの関数をより汎用性のあるものにするためには、配列の各要素をとって計算し、Int型を返す関数を第二引数として受けとる必要があります。
func computeIntArray(xs: [Int], transform: Int -> Int) -> [Int] { var result: [Int] = [] for x in xs { result.append(transform(x)) } return result }
しかし、このコードはまだ最も柔軟であるとは言えません。 例えば、配列の各要素を偶数であるか判定し、真偽値の値を配列に入れて返したい場合はどうすれば良いでしょうか。
この問題を解決してくれるのがジェネリクス型です。
func genericComputeArray<T>(xs: [Int], transform: Int -> T) -> [T] { var result: [T] = [] for x in xs { result.append(transform(x)) } return result }
これで任意の型で動作する関数を書くことができました。
この関数はさらに抽象化することができます。入力される配列は[Int]である必要はありません。
func map<Element, T>(xs: [Element], transform: Element -> T) -> [T] { var result: [T] = [] for x in xs { result.append(transform(x)) } return result }
グローバルレベルでmap関数を定義するのではなく、Arrayの拡張として定義することでSwiftにうまくフィットします。
extension Array { func map<T>(transform: Element -> T) -> [T] { var result: [T] = [] for x in self { result.append(transform(x)) } return result } }
map関数はSwiftの標準ライブラリに含まれているので自分で実装する必要はありませんが、map関数はやろうと思えば簡単に実装できることは示したのです。
Filter
あるディレクトリにこんなファイルがあるとします。
let exampleFiles = ["README.md", "HelloWorld.swift", "FlappyBird.swift"]
この中からSwiftファイルだけを取り出したい場合、シンプルな配列で書くことができます。
func getSwiftFiles(files: [String]) -> [String] { var result: [String] = [] for file in files { if file.hasSuffix(".swift") { result.append(file) } } return result }
これを抽象化するとFilter関数ができます。
extension Array { func filter(includElement: Element -> Bool) -> [Element] { var result: [Element] = [] for x in self where includElement(x) { result.append(x) } return result } }
Reduce
配列内の合計を計算する関数を作るのは簡単です。
func sum(xs: [Int]) -> Int { var result: Int = 0 for x in xs { result += x } return result }
抽象化してReduce関数ができます。
extension Array { func reduce<T>(initial: T, combine: (T, Element) -> T) -> T { var result = initial for x in self { result = combine(result, x) } return result } }
Putting It All Together
締めとして、map, filter, reduceを使ったささやかな例を紹介します。
都市名と人口を持つ構造体があります。
struct City { let name: String let population: Int }
都市をいくつか定義します。
let paris = City(name: "Paris", population: 2241) let madrid = City(name: "Madrid", population: 3165) let amsterdam = City(name: "Amsterdam", population: 827) let berlin = City(name: "Berlin", population: 3562) let cities = [paris, madrid, amsterdam, berlin]
ここで、100万人以上の人口を持つ都市を人口と一緒に出力したいと思います。
まず、人口の単位を返還するヘルパー関数を定義します。
extension City { func cityByScalingPopulation() -> City { return City(name: name, population: population * 1000) } }
ここで本章で紹介したすべてのパーツを使って、次のようなコードが書けます。
cities.filter { $0.population > 1000 } .map { $0.cityByScalingPopulation() } .reduce("") { result, c in result + "\n" + "\(c.name): \(c.population)" }
Paris: 2241000 Madrid: 3165000 Berlin: 3562000
Swift標準ライブラリの持つmap, filter, reduceをうまくチェーンできました。
Functional Swift 3章 "Wrapping Core Image" まとめ
この章ではCore ImageをfunctionalにラッピングするAPIを構築することで、より実戦的に高階関数と関数合成を利用する方法を学びます。
Filter型
typealias Filter = CIImage -> CIImage
オリジナルの
let filter = CIFilter(name: "CIVignette")
などのkey値で初期化するCIFilterをカプセル化するためにFilter型を関数として定義。
フィルタの構築
Filter型を定義したので、個別のFilterを定義していく
全て
func myFilter("パラメータ") -> Filter
の形
func blur(radius: Double) -> Filter { return { image in let parameters = [ kCIInputRadiusKey: radius, kCIInputImageKey: image ] guard let filter = CIFilter(name: "CIGaussianBlur", withInputParameters: parameters) else { fatalError() } guard let outputImage = filter.outputImage else { fatalError() } return outputImage } }
引数はぼかしの半径のみです。 CIImage型のimageをとり、新しいCIImageを返す関数(先ほど定義したFilter(CIImage -> CIImage))を返します。 同じパターンで様々なフィルタ関数を定義できます。
func chrome() -> Filter { return { image in let parameters = [ kCIInputImageKey: image ] guard let filter = CIFilter(name: "CIPhotoEffectChrome", withInputParameters: parameters) else { fatalError() } guard let outputImage = filter.outputImage else { fatalError() } return outputImage } }
フィルタを合成
ここまで作成したぼかしとクロームのフィルタ関数を合成していきます。
let url = NSURL(string: "http://www.objc.io/images/covers/16.jpg") let image = CIImage(contentsOfURL: url!)! let blurredImage = blur(5.0)(image) let chromedImage = chrome()(blurredImage)
一度ブラーをかけたimageに新たにクロームフィルタをかけて画像を生成しています。 もちろん一つにまとめることもできますが、括弧が増え途端に読めなくなってしまいます。
let result = chrome()(blur(5.0)(image))
そこで、カスタムオペレーションを定義することで読みやすさを保つことができます。
infix operator >>> { associativity left } func >>>(filter1: Filter, filter2: Filter) -> Filter { return { image in filter2(filter1(image)) } } let filter = chrome() >>> blur(5.0) let result = filter(image)
SwiftBondでAPIからデータバインディング
データバインディングのライブラリとしてBondを初めてつかってみました。
いろいろやってみるうちに、MVVM風にViewModelからデータをバインディングする設計が良い感じで気に入りました
サンプルコードはgithubを参照
実装
iTuens search apiを使って、アーティストと曲名をTableViewに表示します
Model
import SwiftyJSON struct List { let trackName: String let artistName: String init(json: JSON) { self.trackName = json["trackName"].stringValue self.artistName = json["artistName"].stringValue } }
View Model
モデルをObservableArrayで保持し、apiコールで更新します。
import UIKit import Bond import Alamofire import SwiftyJSON class ListViewModel { internal let lists = ObservableArray<List>() private let urlString = "https://itunes.apple.com/search" private let parameters = ["term":"Swift", "entity":"musicTrack", "limit":"15"] internal func reload() { Alamofire.request(.GET, urlString, parameters: parameters).validate().responseJSON { response in switch response.result { case .Success: if let value = response.result.value { let jsons = JSON(value) for i in 0...jsons["results"].count { let list = List(json: jsons["results"][i]) self.lists.append(list) } } case .Failure(let error): print(error) } } } }
View Controller
ObservableArray<List>
をtableViewにBindします
import UIKit import Bond class ListViewController: UIViewController { @IBOutlet var tableView: UITableView! private let listViewModel = ListViewModel() private var list = ObservableArray<ObservableArray<List>>() override func viewDidLoad() { super.viewDidLoad() self.list = [listViewModel.lists] listViewModel.reload() list.bindTo(tableView) { indexPath, dataSources, tableView in let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell let dataSource = dataSources[indexPath.section][indexPath.row] cell.textLabel?.text = dataSource.trackName cell.detailTextLabel?.text = dataSource.artistName return cell } } }
SwiftでアプリからTwitterにGif投稿
アプリで撮影した動画をgifに変換し、twitterに投稿してみた。
TwitterはGifをサポートしてるけど、既存のSLComposeViewController
や、UIActivityViewController
はgifをサポートしていないので少し面倒でした。
動画をgifに変換
Regiftというライブラリを使ったら簡単にできた
twitterに投稿
まずはActionSheet
で全アカウントを表示し、選択されたaccountを返します
import Accounts func selectAccount() -> AnyObject { let account = ACAccountStore() let accountType = account.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter) account.requestAccessToAccountsWithType(accountType, options: nil, completion: { (success: Bool, error: NSError!) -> Void in if success { guard let accounts = account.accountsWithAccountType(accountType) else { return } if !accounts.isEmpty { let actionSheet = UIAlertController(title: "Choose Account", message: "", preferredStyle: .ActionSheet) for account in accounts { guard let name = account.username else { return } let action = UIAlertAction(title: name, style: .Default, handler: { (action: UIAlertAction) in return account }) actionSheet.addAction(action) } self.presentViewController(actionSheet, animated: true, completion: nil) } } }) } }
選択されたアカウント、メッセージ、gifのNSDataでポスト。 メッセージはDictionaryで["status", ツイート内容]
func post(account: ACAccount, message: [String: String!], data: NSData) { let url = NSURL(string: "https://api.twitter.com/1.1/statuses/update_with_media.json") let postRequest = SLRequest(forServiceType: SLServiceTypeTwitter, requestMethod: .POST, URL: url, parameters: message) postRequest.account = account postRequest.addMultipartData(data, withName: "media", type: "image/gif", filename: "image.gif") postRequest.performRequestWithHandler({ (responseData: NSData!, urlResponse: NSHTTPURLResponse!, error: NSError!) -> Void in print(urlResponse) }) }
SwiftでAWS S3へデータをアップロード
AWS SDK for iOSを使ってアプリからAWS S3に動画をアップロードする処理をやってみた。 ローカルのデータをアップロードして、保存先のURLを取得します。
準備
pod 'AWSS3'
ユーザー認証の仕組みとかを実装できる Amazon CognitoというAWSサービスを登録する必要もあります。
S3の設定 Bucketを作成します。 permissionはとりあえず、EveryOne が UploadとRead できるようにしておきます。
これで準備完了。
実装
application:didFinishLaunchingWithOptions
でCognitoを使ってSDKのセットアップ
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: RegionType, identityPoolId: IdentityPoolId) let configuration = AWSServiceConfiguration(region: DefaultServiceRegionType, credentialsProvider: credentialsProvider) AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = configuration
実際にアップロードするところは
internal func upload() -> NSURL { let uploadRequest = AWSS3TransferManagerUploadRequest() uploadRequest.body = origin uploadRequest.key = remote uploadRequest.bucket = BucketName let transferManager = AWSS3TransferManager.defaultS3TransferManager() transferManager.upload(uploadRequest).continueWithBlock { task -> AnyObject! in if task.result != nil { if let bucket = uploadRequest.bucket, key = uploadRequest.key { let url = NSURL(string: "http://s3.amazonaws.com/\(bucket)/\(key)") return url } } } }
origin
にアップロードするデータのfilepath、remote
にS3に保存するデータの名前を入れます。
アップロードが成功すると保存先のURLが返ってきます。
使いやすいように、SwiftTaskを使ってこんな感じのクラスを作ってみた
import AWSS3 import SwiftTask class S3Uploader { typealias UploadTask = Task<Void, NSURL, NSError> private var origin: NSURL? private var remote: String? internal init(origin: NSURL, remote: String) { self.origin = origin self.remote = remote } internal func upload() -> UploadTask { return UploadTask { progress, fulfill, reject, configure in guard let origin = self.origin, remote = self.remote else { return } let uploadRequest = AWSS3TransferManagerUploadRequest() uploadRequest.body = origin uploadRequest.key = remote uploadRequest.bucket = AppConfig.AWSS3.BucketName let transferManager = AWSS3TransferManager.defaultS3TransferManager() transferManager.upload(uploadRequest).continueWithBlock { task -> AnyObject! in if let error = task.error { reject(error) } if task.result != nil { if let bucket = uploadRequest.bucket, key = uploadRequest.key { if let url = NSURL(string: "http://s3.amazonaws.com/\(bucket)/\(key)") { fulfill(url) } } } return nil } } } }
けっこう頻繁に更新されている公式のサンプルもあって、参考になった。(日本人の方がつくってるっぽい)
SwiftでLINEの桜が降るエフェクトを作ってみた
LINE桜降ってる🌸 pic.twitter.com/JPoHU7hH6k
— ゆづちゃん Luce/BÜMP (@ahonekoyuzuchan) 2016年4月1日
これです。
ほんとはLINEに実装される前にCocoaPodsになにか公開しようと思って作ったものなんだけど...
CoreAnimation
のパーティクルシステム、CAEmitterLayer
とCAEmmiterCell
を使ってけっこう簡単にできました。
桜の他にも、タンポポとPlumも選べるようにした
SwiftでSnapChatっぽいUI
SwiftでSnapChatぽいUI、左右のスワイプでViewControllerが切り替わるやつをやってみた。
まずは大元となるContainerViewController
を作って(Containerという名前が正しいかはわからないけど)、その上に敷いたScrollView
にVCたちをaddChildViewController
していく感じ。
class ContainerViewController: UIViewController { private var offset = 0 private var viewControllers = [UIViewController]() internal init(controllers: [UIViewController], offset: Int) { super.init(nibName: nil, bundle: nil) controllers.forEach { viewControllers.append($0) } self.offset = offset } private func setupScrollView() { let viewBounds = self.view.bounds let viewWidth = viewBounds.width let scrollView = UIScrollView() scrollView.delegate = self scrollView.pagingEnabled = true scrollView.showsHorizontalScrollIndicator = false scrollView.bounces = false scrollView.frame = CGRect(x: viewBounds.origin.x, y: viewBounds.origin.y, width: viewWidth, height: CGRectGetHeight(viewBounds)) self.view.addSubview(scrollView) scrollView.contentSize = CGSize(width: CGFloat(viewControllers.count) * viewWidth, height: CGRectGetHeight(viewBounds)) for (index, vc) in viewControllers.enumerate() { vc.view.frame = CGRect(x: CGFloat(index)*viewWidth, y: 0, width: viewWidth, height: CGRectGetHeight(viewBounds)) self.addChildViewController(vc) scrollView.addSubview(vc.view) vc.didMoveToParentViewController(self) scrollView.sendSubviewToBack(vc.view) } scrollView.contentOffset.x = viewControllers[offset].view.frame.origin.x } }
ContainerViewController
の初期化はアプリ起動時にこんな感じで行ってる。
internal func setup(application: UIApplication) { let cameraSB = UIStoryboard(name: "Camera", bundle: nil) let feedSB = UIStoryboard(name: "Feed", bundle: nil) let profileSB = UIStoryboard(name: "Profile", bundle: nil) let camera = cameraSB.instantiateViewControllerWithIdentifier("camera") let feed = feedSB.instantiateViewControllerWithIdentifier("feed") let profile = profileSB.instantiateViewControllerWithIdentifier("profile") let controllers = [feed, camera, profile] let container = ContainerViewController(controllers: controllers, offset: 1) self.window?.rootViewController = container self.window?.makeKeyAndVisible() }
(実際のコードから削っているのでそのままだと動かないかもしれないです)