20190228のSwiftに関する記事は9件です。

表示される値がアニメーションしながら変わるUILabelを作ってみた

はじめに

アプリを作っていると

UILabelで表示される値がぱっと切り替わると機械的だなー

と感じる箇所がちょいちょい出てきました。

なめらかに変わったら見るの楽しくなるなー、変わってる感もわかりやすいなー

と、ふと思ったので、作ってみることにしました。

結果

変化方法等、諸々はご自由に変化していただくとして()

Feb-28-2019 21-34-05.gif

実装

まず実装を見せますぜ。

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を、アニメーションしながら表示することに成功しました。

(コンマがついているのは、ここに書いたコードにはない機能です←)

最後に

もちろん、機械的が悪いとかそういう話では一切ありません。

し、全てがアニメーションすることが正しいわけでもないと思います。

が、抱いた不満や思いついたことを試してみて、体験価値的に良さげだったらプロダクトに導入する、そんな試みをひたすらやる旅も悪くないなって、つまりはそういうことです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UITableViewCell内のVertical UIStackViewをアニメーションさせる

UITableViewCellの高さが変わるアニメーションを行うには、UITableView.beginUpdates UITableView.endUpdatesでCell高さ変更処理を挟むことで実現できます。

    tableView.beginUpdates()
    // Cell高さが変更されるような処理
    tableView.endUpdates()

ここで、Cell内部にある、VerticalなUIStackViewの一部要素の表示非表示を切り替えてアニメーションする場合、UITableView側のアニメーション設定と合わせて、UIStackView側のアニメーション設定が必要で、Duration設定を正しく設定しないと、アニメーションが安定しません。

IMB_vw5oer.gif

結論から言うと、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.swift
import 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.swift
import 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()
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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;
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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があまりにも早く誤差が大きそうなので
同じ配列でもう一度 forreduceは100回、Setは1000回、回してみます。

for方式 Array::reduce方式 Set方式
77.56(sec) 75.81(sec) 0.30(sec)

forArray::reduceはほぼ同速
Setが250倍ほど早いですね。

結論

ここまで違うとは思っていませんでした。
Swiftでユニークな配列を作るときは間違いなくSetを使ったほうが良さそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

事前に.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ファイルが読み込まれます

aaaaa.png

PrefixHeader.pch

#ifndef PrefixHeader_txt
#define PrefixHeader_txt

 // ここに書くことで事前にhファイルを読みこませることができる
 // 認識される

 #import "test.h"

#endif /* PrefixHeader_txt */
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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()
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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()
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift4.2】DateFormatter使うときはもちろん12時間表示のこと考えてますよね?

iOSの設定 > 日付と時刻には悪魔の設定があります。
そう、12時間表示です
IMG_3D59590EACDF-1.jpeg

今回は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

HHkkなんかが使えそうです。
あとは、
formatter.dateFormat = "HH:mm"
のようにフォーマットを指定すればうまくいきそうですね。
.dateFormatを指定するのはアンチパターンだというのは有名ですが、
時間を抜き出す目的ならば問題なさそうです。









しかし、結論から言えばこれは失敗します!
これでは午前・午後がついたままで、出力は先ほどと同様です。(これが仕様なのかはわからないです)

解決策

.localeNSLocale.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"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Instagramストーリーズに画像や動画を渡す

iphonexspacegrey_portrait.png

はじめに

Spotifyで聴いている音楽のジャケットをステッカーとして共有できたりなど、シェア項目にインスタグラムストーリーズを採用しているアプリが増えていますよね。僕自身も、いくつかの個人アプリに採用していますが、24時間で消滅するがゆえに、気軽にシェアしていただけるので、おススメです。

それらは、URLスキームとUIPasteboardの組み合わせで実装可能です。クリップボードにはこういった使い方もあるのですね、下記の公式ドキュメントを参考にSwift4で実装しています。

ストーリーズへのシェア - Instagramプラットフォーム

インスタグラムストーリーズに受け渡し可能なパラメータは以下の通りです。

  • 必須項目(どれか1つ以上)
    • バックグラウンド画像アセット
    • バックグラウンド動画アセット
    • スタンプアセット
  • オプション項目
    • バックグラウンドレイヤーのトップカラー
    • バックグラウンドレイヤーのボトムカラー
    • アトリビューションURL (承認済みのアプリに限り投稿にURLがリンクされます)

実装コード

Info.plistのLSApplicationQueriesSchemesキーにinstagram-storiesを追加

InstaStories.swift
import 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")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む