- 投稿日:2019-02-28T21:54:52+09:00
表示される値がアニメーションしながら変わるUILabelを作ってみた
はじめに
アプリを作っていると
UILabelで表示される値がぱっと切り替わると機械的だなー
と感じる箇所がちょいちょい出てきました。
なめらかに変わったら見るの楽しくなるなー、変わってる感もわかりやすいなー
と、ふと思ったので、作ってみることにしました。
結果
変化方法等、諸々はご自由に変化していただくとして()
実装
まず実装を見せますぜ。
final class AnimateValueLabel: UILabel { private var loop : CADisplayLink? private var startTime : TimeInterval = 0.0 private var currentValue : Int = 0 private var newValue : Int = 0 func setData(value: Int, animated: Bool = false) { self.newValue = value if animated && self.currentValue != self.newValue { self.animateValue() } else { self.currentValue = value self.setText(value: self.currentValue) } } private func animateValue() { self.startTime = Date.timeIntervalSinceReferenceDate self.loop = CADisplayLink(target: self, selector: #selector(self.update)) if #available(iOS 10.0, *) { self.loop?.preferredFramesPerSecond = 60 } else { self.loop?.frameInterval = 1 } self.loop?.add(to: .current, forMode: .common) self.loop?.isPaused = false } @objc private func update(displayLink: CADisplayLink) { let duration : TimeInterval = 0.3 let elapsed : TimeInterval = Date.timeIntervalSinceReferenceDate - self.startTime let progress: Int = Int(Easing.easeIn.quart.getProgress(elapsed: elapsed, duration: duration, startValue: CGFloat(self.currentValue), endValue: CGFloat(self.newValue))) self.setText(value: progress) if duration < elapsed { self.loop?.isPaused = true self.loop?.invalidate() self.currentValue = self.newValue } } private func setText(value: Int) { self.text = value } }CADisplayLinkやEasingに関しては過去記事を参考にしていただくとして、
https://qiita.com/haguhoms/items/c87d335756042bc867c4
https://qiita.com/haguhoms/items/abc5635e8fa95719cb12これで、setDataに渡したvalueを、アニメーションしながら表示することに成功しました。
(コンマがついているのは、ここに書いたコードにはない機能です←)
最後に
もちろん、機械的が悪いとかそういう話では一切ありません。
し、全てがアニメーションすることが正しいわけでもないと思います。
が、抱いた不満や思いついたことを試してみて、体験価値的に良さげだったらプロダクトに導入する、そんな試みをひたすらやる旅も悪くないなって、つまりはそういうことです。
- 投稿日:2019-02-28T21:11:50+09:00
UITableViewCell内のVertical UIStackViewをアニメーションさせる
UITableViewCell
の高さが変わるアニメーションを行うには、UITableView.beginUpdates
UITableView.endUpdates
でCell高さ変更処理を挟むことで実現できます。tableView.beginUpdates() // Cell高さが変更されるような処理 tableView.endUpdates()ここで、Cell内部にある、Verticalな
UIStackView
の一部要素の表示非表示を切り替えてアニメーションする場合、UITableView
側のアニメーション設定と合わせて、UIStackView
側のアニメーション設定が必要で、Duration設定を正しく設定しないと、アニメーションが安定しません。結論から言うと、
UITableView
側のアニメーションのDuration: 0.3
と同じ時間となるよう、UIStackView
側のDurationを調整すると、アニメーションが安定します。上記Gifの実装では、Verticalな
UIStackView
へ5つのUILabel
を追加し、そのうち、2〜4番目のUILabel
の表示非表示を切り替えています。5つのCellに対して、0.1~0.5のDuration
を設定しています。
UITableView
側のDuration
と揃っていないCellのアニメーションはご覧のとおり、アニメーション中に一部の'UILabel`が消えたり上下にぐらついて見えます。Githubにサンプルコードをおいておきました。
https://github.com/iincho/UITableViewAnimation以下、サンプルコードの一部です。
ViewController.swiftimport UIKit struct CellState { var isOpen: Bool let backgoundColor: UIColor let duration: TimeInterval } class ViewController: UIViewController { @IBOutlet weak var tableView: UITableView! private let cellStates: [CellState] = { return [ CellState(isOpen: true, backgoundColor: .white, duration: 0.1), CellState(isOpen: true, backgoundColor: .lightGray, duration: 0.2), CellState(isOpen: true, backgoundColor: .white, duration: 0.3), CellState(isOpen: true, backgoundColor: .lightGray, duration: 0.4), CellState(isOpen: true, backgoundColor: .white, duration: 0.5), ] }() override func viewDidLoad() { super.viewDidLoad() tableView.delegate = self tableView.dataSource = self let className = "AnimationCell" let nib = UINib(nibName: className, bundle: nil) tableView.register(nib, forCellReuseIdentifier: className) tableView.estimatedRowHeight = 200 tableView.rowHeight = UITableView.automaticDimension tableView.tableFooterView = UIView() tableView.reloadData() } } extension ViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return cellStates.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "AnimationCell", for: indexPath) as! AnimationCell var state = cellStates[indexPath.row] cell.refresh(isOpen: state.isOpen, backgroundColor: state.backgoundColor, duration: state.duration) cell.toggleButtonTapHandler = { [unowned self] in state.isOpen = !state.isOpen self.tableView.beginUpdates() cell.toggle(isOpen: state.isOpen) self.tableView.endUpdates() } return cell } }AnimationCell.swiftimport UIKit class AnimationCell: UITableViewCell { var toggleButtonTapHandler: (() -> Void)? @IBOutlet private weak var toggleButton: UIButton! @IBOutlet private weak var stackView: UIStackView! @IBOutlet private weak var label1: UILabel! @IBOutlet private weak var label2: UILabel! @IBOutlet private weak var label3: UILabel! @IBOutlet private weak var label4: UILabel! @IBOutlet private weak var label5: UILabel! private lazy var animationLabels: [UILabel] = [label2, label3, label4] private var duration: TimeInterval = 0 @IBAction func toggle(_ sender: Any) { toggleButtonTapHandler?() } override func awakeFromNib() { super.awakeFromNib() selectionStyle = .none } override func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) } func refresh(isOpen: Bool, backgroundColor: UIColor, duration: TimeInterval) { contentView.backgroundColor = backgroundColor self.duration = duration refreshTitle(isOpen: isOpen) animationLabels.forEach { $0.isHidden = isOpen } } private func refreshTitle(isOpen: Bool) { let toggleStr = isOpen ? "▼" : "▲" let title = "Duration: \(duration) " + toggleStr toggleButton.setTitle(title, for: .normal) } func toggle(isOpen: Bool) { refreshTitle(isOpen: isOpen) /// Ainmation let opacity: Float = isOpen ? 0.0 : 1.0 UIView.animate(withDuration: duration) { self.animationLabels.forEach { label in label.isHidden = isOpen label.layer.opacity = opacity } self.stackView.layoutIfNeeded() } } }
- 投稿日:2019-02-28T17:36:12+09:00
Provided bucket:""does not match the Storage bucket of the current instance:""
StorageInstance
firebaseを導入する際に
FirebaseApp.configure()
メソッドでインスタンス化されるバケット名はfirebaseProjectのstorageで初めに取得されるバケットになる。ここからバケットのlocationなやバケット名を変更したい場合は別のstorageバケットを作成する必要があり、新しいバケットをインスタンス化しようとしたらProvided bucket:""does not match the Storage bucket of the current instance:""
というエラーが。インスタンスはAPIによってデフォルトのバケットで必ず行われるらしいので。storage()
を変更するとうまくいく。記述
let storageRef = Storage.storage(url: "bucketName").reference()最後に
エラー吐き出し口がこんな感じで、デフォルトがない場合storageクラスがstandardになってるものがインスタンス化される、クラスはgoogleCloudで簡単に変更可能。
FIRStorage.m+ (instancetype)storageForApp:(FIRApp *)app { if (app.options.storageBucket) { NSString *url = [app.options.storageBucket isEqualToString:@""] ? @"" : [@"gs://" stringByAppendingString:app.options.storageBucket]; return [self storageForApp:app URL:url]; } else { NSString *const kAppNotConfiguredMessage = @"No default Storage bucket found. Did you configure Firebase Storage properly?"; [NSException raise:NSInvalidArgumentException format:kAppNotConfiguredMessage]; return nil; } }
- 投稿日:2019-02-28T17:01:53+09:00
[Swift]配列からユニーク値を取り出す時の速度比較
動機
あるプロジェクトで楽曲一覧を表示する時、ユニークなリストの生成があまりにも遅かったので、色々速度比較をしてみました。
実験
以下の方法で100回繰り返してみます。
(array
は0から1000のランダムな配列です。)方法1
オーソドックスに
for
で回してみます。var uniqueArray = [Int]() for i in array{ if !uniqueArray.contains(i) { uniqueArray.append(i) } }方法2
Array::reduceを使ってみます。
array.reduce(into: [Int]()) {a,b in a.contains(b) ? () : a.append(b) }方法3
標準ライブラリの
Set
を使ってみます。Array(Set(array))結果
for
方式Array::reduce
方式Set
方式7.37(sec) 7.58(sec) 0.03(sec) Setがあまりにも早く誤差が大きそうなので
同じ配列でもう一度for
、reduce
は100回、Set
は1000回、回してみます。
for
方式Array::reduce
方式Set
方式77.56(sec) 75.81(sec) 0.30(sec)
for
とArray::reduce
はほぼ同速
Set
が250倍ほど早いですね。結論
ここまで違うとは思っていませんでした。
Swift
でユニークな配列を作るときは間違いなくSet
を使ったほうが良さそうです。
- 投稿日:2019-02-28T16:56:59+09:00
事前に.hファイルを読み込ませる
事前に.hファイルを読み込ませる
Swiftの場合ファイルをファイルツリーにドラッグすると自動で読み込まれますが、
Objective-Cファイルの場合はPrecompile Prefix Headerにヘッダーファイルのimport文を書くことで自動で読み込まれます。Precompile Prefix Headerに記述する
PrefixHeader.pchをファイルから作成
Precompile Prefix Header - YES
Prefix Header - $(SRCROOT)/PrefixHeader.pch
事前にtest.hファイルが読み込まれますPrefixHeader.pch #ifndef PrefixHeader_txt #define PrefixHeader_txt // ここに書くことで事前にhファイルを読みこませることができる // 認識される #import "test.h" #endif /* PrefixHeader_txt */
- 投稿日:2019-02-28T16:29:30+09:00
Promise<->Observable相互変換
はじめに
この記事ではSwiftで非同期な処理を扱う際に便利なライブラリであるRxSwiftとPromiseKitをプロジェクト内で両方導入している際に連携を取る方法について説明します。
導入するにしても大抵は片方しか使わないですが、両者が混在するプロジェクトに向き合わなければならない局面は人生においてあったりなかったりします。
使用ライブラリとバージョンは以下になります。https://github.com/ReactiveX/RxSwift (4.3.1)
https://github.com/mxcl/PromiseKit (6.8.3)ライブラリのインストール手順等については他の方の記事を参照という次第でよろしくおねがいします。
Observable->Promise変換
ObservableをPromiseで使える。Promiseの都合上結果は1度しか受け取れないのでそのようになっています
import PromiseKit import RxSwift extension Observable { func toPromise() -> Promise<Element> { var disposable: Disposable? return Promise { resolver in disposable = take(1) .subscribe( onNext: { value in resolver.fulfill(value) }, onError: { resolver.reject($0) }, onCompleted: { disposable?.dispose() } ) } } }Observable->Promise変換
Observableの結果をPromiseのfulfillとして扱う。Promiseの都合上結果は1度しか受け取れないのでそのようになっています
import PromiseKit import RxSwift extension Observable { func toPromise() -> Promise<Element> { var disposable: Disposable? return Promise { resolver in disposable = take(1) .subscribe( onNext: { value in resolver.fulfill(value) }, onError: { resolver.reject($0) }, onCompleted: { disposable?.dispose() } ) } } }Promise->Observable変換
この変換方式では1度値が流れるとonCompletedを発するので、RxSwiftで継続して値を受け取るにはflatMap等でラップする必要があります
extension Promise { func toObservable() -> Observable<T> { return Observable.create { observer -> Disposable in self.done { observer.onNext($0) } .catch { observer.onError($0) } .finally { observer.onCompleted() } return Disposables.create() } } }
- 投稿日:2019-02-28T16:29:30+09:00
[Swift]Observable(RxSwift) - Promise(PromiseKit)相互変換
この記事ではSwiftで非同期な処理を扱う際に便利なライブラリであるRxSwiftとPromiseKitをプロジェクト内で両方導入している際に連携を取る方法について説明します。
導入するにしても大抵は片方しか使わないですが、両者が混在するプロジェクトに向き合わなければならない局面は人生においてあったりなかったりします。
使用ライブラリとバージョンは以下になります。https://github.com/ReactiveX/RxSwift (4.3.1)
https://github.com/mxcl/PromiseKit (6.8.3)ライブラリのインストール手順等については他の方の記事を参照という次第でよろしくおねがいします。
Observable->Promise変換
ObservableをPromiseで使える。Promiseの都合上結果は1度しか受け取れないのでそのようになっています
import PromiseKit import RxSwift extension Observable { func toPromise() -> Promise<Element> { var disposable: Disposable? return Promise { resolver in disposable = take(1) .subscribe( onNext: { value in resolver.fulfill(value) }, onError: { resolver.reject($0) }, onCompleted: { disposable?.dispose() } ) } } }Promise->Observable変換
この変換方式では1度値が流れるとonCompletedを発するので、RxSwiftで継続して値を受け取るにはflatMap等でラップする必要があります
import PromiseKit import RxSwift extension Promise { func toObservable() -> Observable<T> { return Observable.create { observer -> Disposable in self.done { observer.onNext($0) } .catch { observer.onError($0) } .finally { observer.onCompleted() } return Disposables.create() } } }
- 投稿日:2019-02-28T04:13:20+09:00
【Swift4.2】DateFormatter使うときはもちろん12時間表示のこと考えてますよね?
iOSの
設定 > 日付と時刻
には悪魔の設定があります。
そう、12時間表示です
今回は
DateFormatter
を使っていて陥った2時間表示の罠と、その解決策を備忘録としてまとめました。DateFormatterの落とし穴
わかりやすく
Date型
を下記のようにDateFormatter
を使って時間だけを表示したい場合
を考えたい思います。let formatter = DateFormatter() formatter.timeZone = .current formatter.locale = .current formatter.timeStyle = .short formatter.dateStyle = .none let timeLabel = formatter.string(from: Date()) print(timeLabel) // 理想の出力) "23:59"12時間表示
ユーザが12時間表示設定している端末では、上記の出力は
//端末は12時間表示設定 let formatter = DateFormatter() formatter.timeZone = .current formatter.timeStyle = .short formatter.dateStyle = .none formatter.locale = Locale(identifier: "ja_JP") let timeLabel = formatter.string(from: Date()) print(timeLabel) // 出力) "午後11:59" formatter.locale = Locale(identifier: "en_US") let timeLabel2 = formatter.string(from: Date()) print(timeLabel2) // 出力) "PM11:59"なんだか余計な"午後"や"PM"といった文字がくっついてきますね。
textLabelの長さが言語によって変わってしまうのでUIによっては不便だと思います。冒頭に書いた純正カレンダー内でも、書き込み時にGoogle Calender APIに渡すDate型の文字列
がバグってしまってることが想像できます。"hh:mm"とか"HH:mm"とかで指定できなかったっけ?
まず考えたのが、なんかフォーマットを大文字の"HH"で指定したら24時間表示にならなかったっけ?
ということです。
実際ICU User Guideを参照すると
Symbol Meaning Example(s) output h hour in am/pm (1~12) h,hh 7,07 H hour in day (0~23) H,HH 0,00 k hour in day (1~24) k,kk 24,24 K hour in am/pm (0~11) K,KK 0,00
HH
やkk
なんかが使えそうです。
あとは、
formatter.dateFormat = "HH:mm"
のようにフォーマットを指定すればうまくいきそうですね。
.dateFormat
を指定するのはアンチパターンだというのは有名ですが、
時間を抜き出す目的ならば問題なさそうです。
しかし、結論から言えばこれは失敗します!
これでは午前・午後
がついたままで、出力は先ほどと同様です。(これが仕様なのかはわからないです)解決策
.locale
にNSLocale.system
を指定することで上手くいきます。定義を
NSLocale.locale
と比べてみると.system
A locale representing the generic root values with little localization..locale
A locale representing the user's region settings at the time the property is read.とあり、どうやら
generic root values
が24時間表記で固定されているようです。//端末は12時間表示設定 let formatter = DateFormatter() formatter.timeZone = .current formatter.timeStyle = .short formatter.dateStyle = .none formatter.locale = NSLocale.system let timeLabel = formatter.string(from: Date()) print(timeLabel) // 出力) "11:59"
- 投稿日:2019-02-28T01:24:11+09:00
Instagramストーリーズに画像や動画を渡す
はじめに
Spotifyで聴いている音楽のジャケットをステッカーとして共有できたりなど、シェア項目にインスタグラムストーリーズを採用しているアプリが増えていますよね。僕自身も、いくつかの個人アプリに採用していますが、24時間で消滅するがゆえに、気軽にシェアしていただけるので、おススメです。
それらは、URLスキームとUIPasteboardの組み合わせで実装可能です。クリップボードにはこういった使い方もあるのですね、下記の公式ドキュメントを参考にSwift4で実装しています。
ストーリーズへのシェア - Instagramプラットフォーム
インスタグラムストーリーズに受け渡し可能なパラメータは以下の通りです。
- 必須項目(どれか1つ以上)
- バックグラウンド画像アセット
- バックグラウンド動画アセット
- スタンプアセット
- オプション項目
- バックグラウンドレイヤーのトップカラー
- バックグラウンドレイヤーのボトムカラー
- アトリビューションURL (承認済みのアプリに限り投稿にURLがリンクされます)
実装コード
Info.plistのLSApplicationQueriesSchemesキーにinstagram-storiesを追加
InstaStories.swiftimport UIKit class InstaStories: NSObject { private let urlScheme = URL(string: "instagram-stories://share")! enum optionsKey: String { case stickerImage = "com.instagram.sharedSticker.stickerImage" case bgImage = "com.instagram.sharedSticker.backgroundImage" case bgVideo = "com.instagram.sharedSticker.backgroundVideo" case bgTopColor = "com.instagram.sharedSticker.backgroundTopColor" case bgBottomColor = "com.instagram.sharedSticker.backgroundBottomColor" case contentUrl = "com.instagram.sharedSticker.contentURL" } /// 背景画像を投稿 func post(bgImage:UIImage, stickerImage:UIImage? = nil, contentURL:String? = nil) -> Bool{ var items:[[String : Any]] = [[:]] //Background Image let bgData = bgImage.pngData()! items[0].updateValue(bgData, forKey: optionsKey.bgImage.rawValue) //Sticker Image if stickerImage != nil { let stickerData = stickerImage!.pngData()! items[0].updateValue(stickerData, forKey: optionsKey.stickerImage.rawValue) } //Content URL if contentURL != nil { items[0].updateValue(contentURL as Any, forKey: optionsKey.contentUrl.rawValue) } let isPosted = post(items) return isPosted } /// 背景動画を投稿 func post(bgVideoUrl:URL, stickerImage:UIImage? = nil, contentURL:String? = nil) -> Bool{ var items:[[String : Any]] = [[:]] //Background Video var videoData:Data? do { try videoData = Data(contentsOf: bgVideoUrl) } catch { print("Cannot open \(bgVideoUrl)") return false } items[0].updateValue(videoData as Any, forKey: optionsKey.bgVideo.rawValue) //Sticker Image if stickerImage != nil { let stickerData = stickerImage!.pngData()! items[0].updateValue(stickerData, forKey: optionsKey.stickerImage.rawValue) } //Content URL if contentURL != nil { items[0].updateValue(contentURL as Any, forKey: optionsKey.contentUrl.rawValue) } let isPosted = post(items) return isPosted } /// ステッカー画像を投稿 func post(stickerImage:UIImage, bgTop:String = "#000000", bgBottom:String = "#000000", contentURL:String? = nil) -> Bool{ var items:[[String : Any]] = [[:]] //Sticker Image let stickerData = stickerImage.pngData()! items[0].updateValue(stickerData, forKey: optionsKey.stickerImage.rawValue) //Background Color items[0].updateValue(bgTop, forKey: optionsKey.bgTopColor.rawValue) items[0].updateValue(bgBottom, forKey: optionsKey.bgBottomColor.rawValue) //Content URL if contentURL != nil { items[0].updateValue(contentURL as Any, forKey: optionsKey.contentUrl.rawValue) } let isPosted = post(items) return isPosted } /// Instagram Storiesへ投稿 private func post(_ items:[[String : Any]]) -> Bool{ guard UIApplication.shared.canOpenURL(urlScheme) else { print("Cannot open \(urlScheme)") return false } let options: [UIPasteboard.OptionsKey: Any] = [.expirationDate: Date().addingTimeInterval(60 * 5)] UIPasteboard.general.setItems(items, options: options) UIApplication.shared.open(urlScheme) return true } } // Singleton extension InstaStories { class var Shared : InstaStories { struct Static { static let instance : InstaStories = InstaStories() } return Static.instance } }使用
以下のように呼び出せます(一例)
UIViewController.swift// 背景画像をシェア let isOpened = InstaStories.Shared.post(bgImage:img)UIViewController.swift// ステッカーをシェア + 背景色指定 let isOpened = InstaStories.Shared.post(stickerImage:img, bgTop:"#330000", bgBottom:"#000000")