- 投稿日: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-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:44:23+09:00
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を押したときに出る画面で確認できる。)1Cocoapodsを使うと出るという情報が多いが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にはそんな機能はない。
よって無視すべきと言う結論に達した。
- 投稿日:2019-02-28T11:38:02+09:00
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
- 投稿日: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")