20190228のiOSに関する記事は7件です。

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で続きを読む

事前に.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で続きを読む

Xcodeの警告の「-pie being ignored. It is only used when linking a main executable」はなんぞや

対処法

無視する

筆者のバージョン

Xcode: 10.1
Cocoapods: 1.6.1

概要

-pieとは、位置に依存しない実行ファイルを生成したいときにつけるコンパイルオプション
実行ファイルを生成する以外の状況で、これを指定しても無視される。
無視されたさいに-pie being ignored. It is only used when linking a main executableという警告が出る。

Xcode上にて、通常のBuildでは発生しない。
ただし、InterfaceBuilderのPreviewのために行われるBuildのときに出る模様。(Xcodeでcmd+9を押したときに出る画面で確認できる。)1

Cocoapodsを使うと出るという情報が多いがCarthage使っていても出るし、手動でFrameworkをインストールしても出る。

  • Project
    • 「BuildSettings」
      • 「Linking」
        • 「Generate Position-Dependent Executable」
      • 「Apple Clang - Code Generation」
        • 「Generate Position-DependentCode」

この設定値がNoになっていると思われる。
Projectを新規作成したときのデフォルト値がNoであるようだ。
Cocoapodsで生成したxcworkspaceから見えるPodsプロジェクトの設定値もNo2

試しにこれらをYESにしたら、警告は消える。
だがそれは、本来デフォルトで位置に依存しない実行ファイルを作るところを、位置に依存する実行ファイルを作るように変更することなので、いろいろ怖い。

本当は、InterfaceBuilderにおいてのみこれをYESに設定したいところだが、Xcodeにはそんな機能はない。

よって無視すべきと言う結論に達した。


  1. これにより、CleanをしてXcodeを再起動すれば解決するという人もいるが、単にそれはInterfaceBuilder用のBuildがClean後に初めて行われる間警告が出ないというだけの話である。Storyboardを選択し、Rebuildが走ると復活する。 

  2. Cocoapodsを最新化し、xcworkspaceを生成し直したら問題が解決されるかもしれないと言っている人がいるが、1.6.1時点で治りはしない。 

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

Unityでモバイル端末のバイブレーションをコントロールする

Unityに用意されているバイブレーションAPI

Handheld.Vibrate ();

これで、1秒くらいのバイブレーションがブーンとなりますが、
使うシーンによってはちょっとマッチしない。
タップだったりに合わせて鳴らすにはもう少し短いものがほしいですよね。

調べるといろいろ出てきて、その寄せ集めの情報にはなりますが、
iOSとAndroidの情報がまとまってるものが意外となかったので書いておきます。

iOS

秒数での制御ができないので、プリセットのリストから選んで鳴らすことになります。

IOSUtil.cs
#if UNITY_IOS && !UNITY_EDITOR
        [DllImport ("__Internal")]
        static extern void _playSystemSound(int n);
#endif

        public static void PlaySystemSound(int n) //引数にIDを渡す
        {
#if UNITY_IOS && !UNITY_EDITOR
            _playSystemSound(n);
#endif
        }
IOSUtil.mm
#import <Foundation/Foundation.h>
#import <AudioToolBox/AudioToolBox.h>

extern "C" void _playSystemSound (int soundId)
{
    AudioServicesPlaySystemSound(soundId);
}

タップに反応する短いものだと 1519 なんかが良さそう。
ただ、ドキュメントを探しても、1519 1520 1521 あたりは載って無くて、
使ってて大丈夫なのかは若干不安なところ..

Android

秒数(ミリ秒)で制御することができます。参考リンクまんまですが。

AndroidUtil.cs
#if UNITY_ANDROID && !UNITY_EDITOR
    public static AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
    public static AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
    public static AndroidJavaObject vibrator = currentActivity.Call<AndroidJavaObject>("getSystemService", "vibrator");
#else
        public static AndroidJavaClass unityPlayer;
        public static AndroidJavaObject currentActivity;
        public static AndroidJavaObject vibrator;
#endif

        public static void Vibrate(long milliseconds)
        {
            if (isAndroid())
                vibrator.Call("vibrate", milliseconds);
            else
                Handheld.Vibrate();
        }
        private static bool isAndroid()
        {
#if UNITY_ANDROID && !UNITY_EDITOR
            return true;
#else
            return false;
#endif
        }

端末によって差異はありそうですが、
試した感じ結構細かい数値の違いでも、体感の変化を感じられました。
上記で記載したiOSの 1519 の感触に近づけるには 3ms くらいの短い指定でもよさそう。

Handheld.Vibrate(); の記述について、
コードに残しておくことでAndroidのパーミッションをUnity側でビルド時に自動で追加してくれます。
手動で管理する場合はこの分岐は省略して良いかと思います。

プレビュー

実機で動かして試せるアプリを用意しました。
https://github.com/mrhdms/VibrationTester
iOS, Androidそれぞれで動きます。

iOSはプリセットリストから選択して再生。(音しかならないもの、音とバイブのもの、バイブレーションだけのものが混在)
Androidは0-1秒の間でスライダで調整しながら確認できます。

参考

http://smartgames.hatenablog.com/entry/2019/02/17/125413
http://greenkour.hateblo.jp/entry/2017/11/20/100000

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