- 投稿日:2019-10-04T22:13:28+09:00
@dynamicMemberLookupを使ってSwiftにKotolinのapplyを実装する
私以外にもKotolinのapplyをSwiftでもやりたいと思ったことのある人はいるでしょう。
例えば以下のようにUILabelを定義する場面を考えます。let label: UILabel = { let it = UILabel() it.text = "label.." return it }()以前までは、以下のようなプロトコルを定義することで
protocol Applicatable {} extension Applicatable { func apply(_ closure: ((Self) -> ())) -> Self { closure(self) return self } } extension NSObject: Applicatable {}それっぽく実装することができました。
let label = UILabel().apply { it in it.text = "label.." }しかし言語の進化により、もっとKotolinのapplyに近づけることができるようになりました。
つい先日DuctTapeというライブラリが公開されたのをきっかけにそれが可能になってたことを知りました。(Swiftのキャッチアップに遅れぎみです)dynamicMemberLookup
Swift4.2で追加された機能ですが
@dynamicMemberLookup struct StringMaker { subscript(dynamicMember value: String) -> String { return value } } let maker = StringMaker() debugPrint(maker.id) // String: id debugPrint(maker.name) // String: nameSwift5.1からKeyPathを使えるようになったみたいです。
@dynamicMemberLookup attribute requires ‘xxx’ to have a ‘subscript(dynamicMember:)’ method that accepts either ‘ExpressibleByStringLiteral’ or a keypathこれを使って参考ライブラリをパクりつつインターフェースの変更を40行程度実装すると
let label = UILabel().apply { $0 .text("Apply Swift") .textAlignment(.center) .textColor(.white) .backgroundColor(.black) } debugPrint(label.text) // Optional(Apply Swift)上記のような実装をすることができました。
一応ソースはGistに公開しておきます。https://gist.github.com/churabou/7243d4ac9121a56513bb16023e3698f7
デモ
final class ViewController: UIViewController { override func loadView() { view = UILabel().apply { $0 .text("Apply Swift") .textAlignment(.center) .textColor(.white) .backgroundColor(.black) } } }
- 投稿日:2019-10-04T22:09:42+09:00
iPhoneからBLEのCurrent Time Serviceで現在時刻を取得する(M5StickC編)
概要
M5Stack/M5StickC で現在時刻を取得する方法として NTP ではなく BLE で取得する方法を探していたところ Current Time Service というのがありました。とりあえず Pythonista で試し返ってくる時刻データの内容を確認するためのサンプルプログラムがこちら。
iPhoneからBLEのCurrent Time Serviceで現在時刻を取得する(Pythonista編)M5StickC でも同様にということで試したのですが、いろいろ苦労することになりました。
- スケッチ例の BLE_Client.ino は使いたいサービスをアドバタイズしているサーバに接続する方式ですが、iPhone の Current Time Service はアドバタイズされていない。
- サーバのデバイス名で接続したかったが、スキャンしても名前が取得できない。
- スキャンしたものから iPhone と思われるものに狙いを定めて(TxPower が 12 というのがそれらしかった)接続すると Service および Characteristic は見つかるが readValue しても中身が空。
- ペアリングが必要らしいと悩んでいたところで M5StickC をサーバーとし、iPhone側からペアリングを行なった後に M5StickC をクライアントとしてペアリングした iPhone に接続する例を見つけ参考にさせていただいた。元は別途 NTP などで時刻合わせの想定だったけど、iPhone との BLE連携アプリなので Current Time Service での時刻取得が最適。
- Notifications From iPhone to ESP32-Watch
https://www.hackster.io/takeru/notifications-from-iphone-to-esp32-watch-251a6e- ペアリングしようとしても iPhone の設定アプリの Bluetooth画面に M5StickC のデバイス名が現れない。Bluetooth のテストアプリ(BLE Scanner等)では見え、そこで接続すると時刻取得に成功。
- 一旦成功した後は Bluetooth画面に名前が現れているので、そこで接続操作を行えばよくなる。
- その後、いろいろ試しているうちに成功した方法でも readValue しても中身が空になる現象が発生。ここで他の M5Stack で試したりしたが問題ない。ふと M5Burner で Erase してみたらその後は正常に値が取得できるようになった。
課題
最初のペアリング時に iPhone側の設定アプリの Bluetooth画面に表示されないのはなぜ?
- 2014年11月の投稿だが、BLEのデバイスは表示されない、とある。
https://stackoverflow.com/questions/27211573/bluetooth-low-energy-advertising-to-be-discoverable-in-ios-settings
Bluetooth Low Energy devices are not discoverable in the Settings->Bluetooth page. This is only for Bluetooth 2.1/3.0 devices such as keyboards and headsets/handsfree devices. A Bluetooth Low Energy peripheral is only discoverable by an app using Core Bluetooth.最初に投稿した時の TFT_Clock.ino ベースのプログラムでは M5StickC の表示時刻が iPhone側より2秒ほど進んでいた。
- loop()の中で時刻取得処理を行っていた部分が悪さをしていたようなので、setup()の中で時刻取得を完了させるようにすることで対応。
Pythonista では iPhone/iPad間でアドバタイズに名前が含まれ、ペアリング無しでデータ取得ができた。iOS間は特別扱い?
- 以下に、バックグラウンドで動くペリフェラルはローカルネームがアドバタイズされない、とある。
そして特別な「overflow」エリアに置かれ、iOSデバイスだけが見ることができる。ずるい…。
Core Bluetooth Programming Guide
Core Bluetooth Background Execution Modes
https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/CoreBluetoothBackgroundProcessingForIOSApps/PerformingTasksWhileYourAppIsInTheBackground.html#//apple_ref/doc/uid/TP40013257-CH7-SW1環境
- iPhone 6 iOS 12.4.2 (BLE Server)
- M5StickC (BLE Client)
- Arduino 1.8.10
- ESP32 Arduino 1.0.3
実行例
- M5StickC 側のアプリ実行後、iPhone側の設定アプリのBluetooth画面には現れない
- Bluetooth のテストアプリ(今回は BLE Scanner を使用)で Connect
- ペアリング要求画面が現れる
- 以降はBluetooth画面にも表示されるようになる
実行時のシリアルモニタ出力例
M5StickC initializing...OK Start Advertising MyServerCallbacks Server Connected: xx:xx:xx:xx:xx:xx MyServerCallbacks Stop Advertising Client Connecting to Server xx:xx:xx:xx:xx:xx MyClientCallback Client Connected Client Connected Get Current Time from Server The characteristic value length: 0 Get Current Time from Server The characteristic value length: 0 Get Current Time from Server The characteristic value length: 0 Get Current Time from Server The characteristic value length: 0 Get Current Time from Server The characteristic value length: 0 Get Current Time from Server The characteristic value length: 0 Get Current Time from Server The characteristic value length: 0 Get Current Time from Server The characteristic value length: 0 Get Current Time from Server The characteristic value length: 10 <-- ペアリング完了後に値が返る 2019-10-04 21:25:28.984 5 x02
M5StickCプログラム
M5StickC用のスケッチ例 TFT_Clock.ino はコンパイル時刻を最初の時刻としますが、BLE Current Time Serviceからの時刻取得処理を追加したプログラムです。
TFT_Clock.ino/* An example analogue clock using a TFT LCD screen to show the time use of some of the drawing commands with the library. For a more accurate clock, it would be better to use the RTClib library. But this is just a demo. This sketch uses font 4 only. Make sure all the display driver and pin comnenctions are correct by editting the User_Setup.h file in the TFT_eSPI library folder. ######################################################################### ###### DON'T FORGET TO UPDATE THE User_Setup.h FILE IN THE LIBRARY ###### ######################################################################### Based on a sketch by Gilchrist 6/2/2014 1.0 */ #include <M5StickC.h> //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++ for Get Current Time from BLE Current Time Service +++++ //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #include <BLEDevice.h> #define LOCAL_NAME "M5StickC Clock" static BLEUUID CTSserviceUUID("1805"); static BLEUUID CTScharUUID("2a2b"); BLEServer *pServer = NULL; BLEClient *pClient = NULL; BLEAddress *pBLEAddress = NULL; class MyClientCallback : public BLEClientCallbacks { void onConnect(BLEClient* pclient) { Serial.println("MyClientCallback Client Connected"); } void onDisconnect(BLEClient* pclient) { Serial.println("MyClientCallback Client Disconnected"); } }; class MyServerCallbacks: public BLEServerCallbacks { void onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) { pBLEAddress = new BLEAddress(param->connect.remote_bda); Serial.printf("MyServerCallbacks Server Connected: %s\n", pBLEAddress->toString().c_str()); pServer->getAdvertising()->stop(); Serial.println("MyServerCallbacks Stop Advertising"); }; void onDisconnect(BLEServer* pServer) { Serial.println("MyServerCallbacks Server Disconnected"); } }; //-------------------------------------------------------------- #define TFT_GREY 0x5AEB float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0; // Saved H, M, S x & y multipliers float sdeg=0, mdeg=0, hdeg=0; uint16_t osx=120, osy=120, omx=120, omy=120, ohx=120, ohy=120; // Saved H, M, S x & y coords uint16_t x0=0, x1=0, yy0=0, yy1=0; uint32_t targetTime = 0; // for next 1 second timeout static uint8_t conv2d(const char* p); // Forward declaration needed for IDE 1.6.x uint8_t hh=conv2d(__TIME__), mm=conv2d(__TIME__+3), ss=conv2d(__TIME__+6); // Get H, M, S from compile time boolean initial = 1; void setup(void) { M5.begin(); // M5.Lcd.setRotation(0); //M5.Lcd.fillScreen(TFT_BLACK); //M5.Lcd.fillScreen(TFT_RED); //M5.Lcd.fillScreen(TFT_GREEN); //M5.Lcd.fillScreen(TFT_BLUE); //M5.Lcd.fillScreen(TFT_BLACK); M5.Lcd.fillScreen(TFT_GREY); M5.Lcd.setTextColor(TFT_WHITE, TFT_GREY); // Adding a background colour erases previous text automatically // Draw clock face //M5.Lcd.fillCircle(120, 120, 118, TFT_GREEN); //M5.Lcd.fillCircle(120, 120, 110, TFT_BLACK); M5.Lcd.fillCircle(40, 40, 40, TFT_GREEN); M5.Lcd.fillCircle(40, 40, 36, TFT_BLACK); // Draw 12 lines for(int i = 0; i<360; i+= 30) { sx = cos((i-90)*0.0174532925); sy = sin((i-90)*0.0174532925); x0 = sx*38+40; yy0 = sy*38+40; x1 = sx*32+40; yy1 = sy*32+40; M5.Lcd.drawLine(x0, yy0, x1, yy1, TFT_GREEN); } // Draw 60 dots for(int i = 0; i<360; i+= 6) { sx = cos((i-90)*0.0174532925); sy = sin((i-90)*0.0174532925); x0 = sx*34+40; yy0 = sy*34+40; // Draw minute markers M5.Lcd.drawPixel(x0, yy0, TFT_WHITE); // Draw main quadrant dots if(i==0 || i==180) M5.Lcd.fillCircle(x0, yy0, 2, TFT_WHITE); if(i==90 || i==270) M5.Lcd.fillCircle(x0, yy0, 2, TFT_WHITE); } M5.Lcd.fillCircle(40, 40, 2, TFT_WHITE); // Draw text at position 120,260 using fonts 4 // Only font numbers 2,4,6,7 are valid. Font 6 only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : . - a p m // Font 7 is a 7 segment font and only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : . //M5.Lcd.drawCentreString("M5Stack",120,260,4); //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //+++++ for Get Current Time from BLE Current Time Service +++++ //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ BLEDevice::init(LOCAL_NAME); BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT); pServer = BLEDevice::createServer(); pServer->setCallbacks(new MyServerCallbacks()); pClient = BLEDevice::createClient(); pClient->setClientCallbacks(new MyClientCallback()); pServer->getAdvertising()->start(); Serial.println("Start Advertising"); while (pBLEAddress == NULL) { delay(100); } Serial.printf("Client Connecting to Server %s\n", pBLEAddress->toString().c_str()); pClient->connect(*pBLEAddress); Serial.println("Client Connected"); Serial.println("Get Current Time from Server"); while (true) { std::string value = pClient->getValue(CTSserviceUUID, CTScharUUID); Serial.printf("The characteristic value length: %d\n", value.length()); if (value.length() == 10) { // 10 byte data // byte 0,1 Year (byte1*256+byte0) // byte 2 Month // byte 3 Day // byte 4 Hours // byte 5 Minutes // byte 6 Seconds // byte 7 Day of Week (Monday = 1, ... Sunday = 7) // byte 8 1/256th of a second (milliseconds = byte8*1000/256) // byte 9 Adjust Reason // bit 0: Manual time update // bit 1: External reference time update // bit 2: Change of time zone // bit 3: Change of DST (daylight savings time) hh = value[4]; mm = value[5]; ss = value[6]; Serial.printf("%d-%02d-%02d %02d:%02d:%02d.%03d %d x%02x\n", value[1] << 8 | value[0], value[2], value[3], value[4], value[5], value[6], value[8]*1000/256, value[7], value[9]); break; } delay(100); } //-------------------------------------------------------------- targetTime = millis() + 1000; } void loop() { if (targetTime < millis()) { targetTime += 1000; ss++; // Advance second if (ss==60) { ss=0; mm++; // Advance minute if(mm>59) { mm=0; hh++; // Advance hour if (hh>23) { hh=0; } } } // Pre-compute hand degrees, x & y coords for a fast screen update sdeg = ss*6; // 0-59 -> 0-354 mdeg = mm*6+sdeg*0.01666667; // 0-59 -> 0-360 - includes seconds hdeg = hh*30+mdeg*0.0833333; // 0-11 -> 0-360 - includes minutes and seconds hx = cos((hdeg-90)*0.0174532925); hy = sin((hdeg-90)*0.0174532925); mx = cos((mdeg-90)*0.0174532925); my = sin((mdeg-90)*0.0174532925); sx = cos((sdeg-90)*0.0174532925); sy = sin((sdeg-90)*0.0174532925); if (ss==0 || initial) { initial = 0; // Erase hour and minute hand positions every minute M5.Lcd.drawLine(ohx, ohy, 40, 40, TFT_BLACK); ohx = hx*15+40; ohy = hy*15+40; M5.Lcd.drawLine(omx, omy, 40, 40, TFT_BLACK); omx = mx*20+40; omy = my*20+40; } // Redraw new hand positions, hour and minute hands not erased here to avoid flicker M5.Lcd.drawLine(osx, osy, 40, 40, TFT_BLACK); osx = sx*25+40; osy = sy*25+40; M5.Lcd.drawLine(osx, osy, 40, 40, TFT_RED); M5.Lcd.drawLine(ohx, ohy, 40, 40, TFT_WHITE); M5.Lcd.drawLine(omx, omy, 40, 40, TFT_WHITE); M5.Lcd.drawLine(osx, osy, 40, 40, TFT_RED); M5.Lcd.fillCircle(40, 40, 2, TFT_RED); } } static uint8_t conv2d(const char* p) { uint8_t v = 0; if ('0' <= *p && *p <= '9') v = *p - '0'; return 10 * v + *++p - '0'; }
- 投稿日:2019-10-04T19:33:30+09:00
SkeletonView
よく商用アプリでも使われているのを見るローディングライブラリである「SkeletonView」を使用したので、使い方の備忘録として書き残しておきます。
import SkeletonViewoverride func viewDidLoad() { super.viewDidLoad() // TableViewの行の高さを可変にする tableView.rowHeight = UITableView.automaticDimension // UITableView.automaticDimensionを設定している場合に、SkeletonViewを使うなら // estimatedRowHeightの設定が必要 tableView.estimatedRowHeight = 140 // スケルトン表示開始 view.showAnimatedGradientSkeleton() }defer { // スケルトン表示終了 self.view.hideSkeleton() }// MARK: - SkeletonTableViewDataSource extension ViewController: SkeletonTableViewDataSource { /// スケルトン表示時のセクション数 func numSections(in collectionSkeletonView: UITableView) -> Int { return 2 } /// スケルトン表示時の行数 func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int { return 2 } /// スケルトン表示する再利用セルのIdentifier func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier { switch indexPath.row { case 0: return "summaryCell" case 1...: return "detailCell" default: DispatchQueue.main.async { self.showAlert(title: "予期せぬエラー", message: "予期しないセルがあります") } return "" } } }参考
https://github.com/Juanpe/SkeletonView
https://dev.classmethod.jp/smartphone/iphone/oss-skeleton-view-introduction/
- 投稿日:2019-10-04T19:24:33+09:00
iPhoneからBLEのCurrent Time Serviceで現在時刻を取得する(Pythonista編)
概要
元は M5Stack で現在時刻を取得する方法として NTP ではなく BLE で取得する方法を探していたところ Current Time Service というのがありました。とりあえず Pythonista で試し返ってくる時刻データの内容を確認するためのサンプルプログラムです。
これでできるなら M5Stack/M5StickC でも同様にできるだろう、と始めたのですが、1週間ほど格闘することになりました。結果的に取得はできたので、それについてはこちら。
iPhoneからBLEのCurrent Time Serviceで現在時刻を取得する(M5StickC編)環境
- Pythonista3 v3.2
- iPhone 6 iOS 12.4.2 (BLE Server)
- iPad mini 4 iOS 13.1.2 (BLE Client)
実行例
iPad上でPythonistaを実行し、iPhoneの時刻をBLE Current Time Serviceで取得
Pythonista実行時コンソール出力Scanning for peripherals... Discovered peripheral: MyiPhone (..........) Connected: MyiPhone Discovering services... CTS Service found: 1805 CTS Characteristics found: 2A2B 2019-10-04 19:02:36.281 (Fri) Adjust Reason: 2 2019-10-04 19:02:37.300 (Fri) Adjust Reason: 2 2019-10-04 19:02:38.292 (Fri) Adjust Reason: 2 2019-10-04 19:02:39.312 (Fri) Adjust Reason: 2 2019-10-04 19:02:40.300 (Fri) Adjust Reason: 2 2019-10-04 19:02:41.320 (Fri) Adjust Reason: 2 2019-10-04 19:02:42.312 (Fri) Adjust Reason: 2 : : :
Pythonista3プログラム
iPhoneの前後左右の傾きに応じて、左右の車輪を動かすための指示をBLE経由で送信する仕組みです。
BLE_Current_Time_Service.pyimport time import cb import struct CTS_DEVICE_NAME = 'MyiPhone' CTS_SERVICE_UUID = '1805' CTS_CHARACTERISTIC_UUID = '2A2B' DAY_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] class MyCentralManagerDelegate (object): def __init__(self): self.peripheral = None self.characteristic = None def did_discover_peripheral(self, p): if p.name and CTS_DEVICE_NAME in p.name and not self.peripheral: # Keep a reference to the peripheral, so it doesn't get garbage-collected: print('Discovered peripheral: %s (%s)' % (p.name, p.uuid)) self.peripheral = p cb.connect_peripheral(self.peripheral) cb.stop_scan() def did_connect_peripheral(self, p): print('Connected: %s' % p.name) print('Discovering services...') p.discover_services() def did_fail_to_connect_peripheral(self, p, error): print('Failed to connect: %s' % (error,)) def did_disconnect_peripheral(self, p, error): print('Disconnected, error: %s' % (error,)) self.peripheral = None self.characteristic = None cb.scan_for_peripherals() def did_discover_services(self, p, error): for s in p.services: if CTS_SERVICE_UUID in s.uuid: print('CTS Service found: %s' % (s.uuid,)) p.discover_characteristics(s) def did_discover_characteristics(self, s, error): if CTS_SERVICE_UUID in s.uuid: for c in s.characteristics: if CTS_CHARACTERISTIC_UUID in c.uuid: print('CTS Characteristics found: %s' % (c.uuid,)) self.characteristic = c def did_update_value(self, c, error): if CTS_CHARACTERISTIC_UUID == c.uuid: # 10 byte data # byte 0,1 Year (byte1*256+byte0) # byte 2 Month # byte 3 Day # byte 4 Hours # byte 5 Minutes # byte 6 Seconds # byte 7 Day of Week (Monday = 1, ... Sunday = 7) # byte 8 1/256th of a second (milliseconds = byte8*1000/256) # byte 9 Adjust Reason # bit 0: Manual time update # bit 1: External reference time update # bit 2: Change of time zone # bit 3: Change of DST (daylight savings time) data = struct.unpack('HBBBBBBBB', c.value) print('{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}.{:03d} ({:s}) Adjust Reason: {:d}'.format( *data[:6], int(data[7]*1000/256), DAY_OF_WEEK[data[6]-1], data[8])) def get_current_time(self): if self.peripheral is not None and self.characteristic is not None: self.peripheral.read_characteristic_value(self.characteristic) return True return False cb_delegate = MyCentralManagerDelegate() print('Scanning for peripherals...') cb.set_central_delegate(cb_delegate) cb.scan_for_peripherals() # Keep the connection alive until the 'Stop' button is pressed: try: while True: cb_delegate.get_current_time() time.sleep(1) except KeyboardInterrupt: # Disconnect everything: cb.reset()
- 投稿日:2019-10-04T17:12:42+09:00
新しいバージョンが利用可能になったことをユーザにお知らせする - Siren
Siren とは?
アプリの新しいバージョンが利用可能になったことをユーザにお知らせアラートを表示するライブラリです。
本内容は Siren 5.2.1 をもとに記述しています。シンプルな実装
import Siren 後、Siren.shared.wail() を didFinishLaunchingWithOptions で呼び出すだけで動作します。
AppDelegate.swiftimport Siren // Line 1 import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { window?.makeKeyAndVisible() Siren.shared.wail() // Line 2 return true } }カスタマイズ
Siren の Manager を設定することで次のようなカスタマイズが可能です。
- JP App Store でバージョンチェックしたい(デフォルトではUS)
- バージョン(メジャー、マイナー、パッチ、リビジョン)毎に細かく表示を分けたい
- アラートの表示頻度を変更したい(起動毎、日に1回、週に1回)
- ユーザの選択肢を制限したい(強制アップデート、次回更新、このバージョンをスキップ)
JP App Store でバージョンチェックしたい
デフォルトでは US App Store に対してバージョンチェックを行います。
日本の App Store に対してバージョンチェックを行いたい場合は日本の App Store 向けに初期化したAPIManagerを用意します。let siren = Siren.shared siren.apiManager = APIManager.init(countryCode: "JP")バージョン(メジャー、マイナー、パッチ、リビジョン)毎に細かく表示を分けたい
RulesManaer を設定してアラートを表示するルールをカスタマイズ出来ます。
メジャー、マイナー、パッチ、リビジョンなど全ての更新に個別のルールを設定出来ます。// 起動ごとにアラート表示、強制アップデートのルール let forceRules = Rules.init(promptFrequency: .immediately, forAlertType: .force) // 1日1回アラート表示、アップデートのタイミングはユーザが選べるルール let optionRules = Rules.init(promptFrequency: .daily, forAlertType: .option) // メジャーバージョンが上がった場合は強制アップデート // マイナーバージョン以下のアップデートの場合はユーザに選択可能とする let siren = Siren.shared siren.rulesManager = RulesManager( majorUpdateRules: forceRules, minorUpdateRules: optionRules, patchUpdateRules: optionRules, revisionUpdateRules: optionRules, showAlertAfterCurrentVersionHasBeenReleasedForDays: 1) )Rules
利用頻度が高そうなルールがあらかじめ用意されています。
Rule 説明 annoying アプリを起動するたびに表示
アプリの更新をスキップ可能critical アプリを起動するたびに表示
アプリの更新を強制default 日に1回表示
アプリの更新をスキップ可能
このバージョンをスキップも可能hinting 週に1回表示
アプリの更新をスキップ可能persistent 日に1回表示
アプリの更新をスキップ可能relaxed 週に1回表示
アプリの更新をスキップ可能
このバージョンをスキップも可能let siren = Siren.shared siren.rulesManager = RulesManager( majorUpdateRules: .critical, minorUpdateRules: .default, patchUpdateRules: .default, revisionUpdateRules: .default, showAlertAfterCurrentVersionHasBeenReleasedForDays: 1) )独自 Rules
バージョンチェックの頻度やタイミング、ユーザへの選択肢を独自に設定することも可能です。
独自ルールで設定できる内容は下記の2つです。promptFrequency
ユーザにアプリ更新を促す頻度です。
パラメタ 説明 immediately アプリを起動するたびに表示 daily 日に1回表示 weekly 週に1回表示 forAlertType
表示するアラートタイプです。ユーザが選べるボタンの数が変わります。
パラメタ 説明 アラート force ユーザにアプリの更新を強制する option 今すぐアプリを更新するか、起動時に更新する
どちらかユーザが選択できるskip ・今すぐアプリを更新する
・次回の起動時に更新する
・このバージョンはスキップする
の内からいずれかをユーザが選択できるreleasedForDays
デフォルトでは更新アラートの表示を1日遅らせているそうです。
(showAlertAfterCurrentVersionHasBeenReleasedForDays: 1)
これは全ての地域の App Store CDN でバイナリが利用可能になる時間を待つためです。
通常、6〜24時間かかるため1日遅らせているようです。
showAlertAfterCurrentVersionHasBeenReleasedForDays に 0 を指定すると即時チェックとなりますがアラートが表示されるのに App Store にアップデートがまだ存在しない状況が発生する可能性があります。Localization
デフォルトではアプリの言語設定に従ってメッセージが表示されます。
設定が存在しない場合は英語で表示されます。
「設定 > 一般 > 言語と地域 > 使用言語」でメッセージが切り替わります。
言語を強制したい場合
例えばどんな場合でも日本語で表示したい場合は PresentationManager を指定します。
siren.presentationManager = PresentationManager(forceLanguageLocalization: .japanese)独自の文言で表示したい場合
あらかじめ用意されたメッセージではなくオリジナルな文言を用いたい場合も PresentationManager を設定します。
ボタンの文字色も変更できます。siren.presentationManager = PresentationManager( alertTintColor: UIColor.orange, appName: "アプリ名", alertTitle: "新しいバージョンがあります", alertMessage: "新バージョンではXXX機能がご利用頂けます", updateButtonTitle: "アップデートする", nextTimeButtonTitle: "今はしない", skipButtonTitle: "次のバージョンまで表示しない", forceLanguageLocalization: .japanese)オリジナルのメッセージには新しいバージョン番号が表示されていますが、独自の文言を設定する場合には新しいバージョン番号を差し込むことは今のところできません。
iOS13 対応
5.2.1 より古いバージョンでは、iOS13でダイアログが表示されてすぐ画面遷移してしまう不具合が発生していますので 5.2.1 以降へアップデートしましょう。
- 投稿日:2019-10-04T16:54:27+09:00
ObjectMapper failed to serialize responseが出たらJSONを疑え
はじめに
APIでJSONを取得してくる時に、静かにエラーになっていた。
ニュースアプリを作っているのですが、記事の筆者の情報が取れていなかった。環境
pod 'AlamofireObjectMapper', '~> 5.2'
pod 'ObjectMapper', '~> 3.4'
Swift4.2
Xcode11
target? iOS:12.1エラーと対処法
NetworkUtils.swiftlet requestReference = Alamofire.request(url, method: .get).responseArray(keyPath: keyPath) { (response: DataResponse<[T]>) -> Void in if let result = response.result.value { observer.onNext( observer.onCompleted() } else if let error = response.result.error { (こっちに入っていた) }errorを見てみると、
↓
ObjectMapper failed to serialize response
とある。調べてみるとJSONの内容が怪しいとのこと。
JSONが正しいか判断してくれるサイト↓
https://jsonlint.com/ここで、無駄な改行を消したらソースは変更せずに解決しました!
- 投稿日:2019-10-04T16:27:36+09:00
Webプログラマがアプリ開発(ios,android)
- swift Objective-cは敷居が高い。
- 使い慣れたVS Codeでコーディングがしたい。
- はじめは簡単なアプリ作りたい
そんな方にReact Native
※少しReactかじった方が良いかも環境
- mac
- XCode シュミレーター必須
React Nativeのはじめ
Expoの方ではなく、「React Native CLI Quickstart」を実施
- homebrewで各インストール
brew install yarn brew install node brew install watchman brew tap AdoptOpenJDK/openjdk brew cask install adoptopenjdk8
- npmでreact-native-cliをグローバルへインストール
npm install -g react-native-cli
- ソースを置くフォルダでinitして実行
react-native init AwesomeProject cd AwesomeProject react-native run-iosReactのモジュールが無いよとビルドエラーになった場合、以下のコマンドで、モジュール取得して再ビルドしたらいけた。
cd ios pod install動いたあとは、App.jsを修正していくことでアプリの開発が行える。 簡単
- 投稿日:2019-10-04T16:16:07+09:00
個人開発のアプリでダークモード対応したので進め方とその所感
はじめに
ついに iOS 13 がリリースされましたね。
私のリリースしているアプリに関しては対応を済ませて
iOS 13 リリース日の 9/20 ギリギリにリリースしました。iOS 13 では大きな変更が多数あり,さらに意図しないところでクラッシュしたり,
変更があったりして今までになく対応を強いられる部分も多かったです?
大きな変更のひとつがダークモード対応ですね。
みなさん記事を書かれているので私もダークモード対応の進め方などについて書こうと思います。キャッチアップ
WWDC 18 で macOS にダークモードが搭載されることが発表され,
いつも通りなら来年は iOS かなと思われていましたが,
やはりiOS 13からダークモード搭載となりました。
(WWDC19のサイトのデザインやもらったアップルロゴの漆黒のピンバッチでも感じられましたね)私はその発表を会場で聞いていてダークモードでの表示にかっけーと感心していました。
早速 Xcode 11 で自分のアプリをビルドしてみて,予想以上の表示崩れに絶望してから
SwiftUI のキャッチアップに移ってしまったので 6月はじめから 8月中旬までほとんど手つかずでした。お盆が終わって業務でやっている案件が落ち着いてきたので,
個人アプリの iOS 13 対応にようやく取り組めた感じです。
そもそもダークモード対応するか,Xcode 11 でビルドしてみた
アプリをしばらく触ってみつつ,案件でも対応することがあるかもしれないし,
どういう感じかまずはみてみようと決めてキャッチアップを始めました。ライトモードだけの選択肢
Apple は全アプリがダークモードに対応することを期待してるよ!と言っているのですが,
今すぐに対応しなくてもライトモードで引き続きアプリをリリース出来なくはないです。
info.plist
にUIUserInterfaceStyle
キーを追加して Value をLight
にすれば良いです。
逆に追加しない場合は Xcode 11 でビルドするとダークモード対応となってしまうということです。ダークモード対応するなら,設定が全画面に及ぶことからデザインは全体的な見直しが必要です。
コードで画面ごとに対応・・・もできますが現実的ではないです。
業務レベルならば,アプリのテーマカラーなどの見直しに始まり,
画像の色あいを変更したり,各画面のデザイン仕様書を作ってから実装を始めるという感じになると思われます。そして実装したはいいが,画面によっては少しテコ入れをーとか普通にありそうです。
一気に対応をするとなると,デザイナーやクライアントの確認を含め,
画面数に比例して結構な工数を準備して取り組む必要がありそうです。私のダークモードの対応手順
どんな表示になるかをまず確認する必要があります。
私の場合,まず Xcode 11 で既存のプロジェクトをビルドして実機で確認するところから始めました。
各画面を表示させてみて,対応が必要な画面をリストアップして画面ごとにチケットとしました。チケットごとにブランチを切って,マージリクエストを作成する際にどういう表示になるのか,
修正前のスクリーンショットとともに貼って確認しました。
(個人開発なのでレビュアも自分。レビューはやらせですw)今回は個人アプリでの対応なのでそこまで深刻に考えずに,
シンプルに対応してみようと考えました。
ライトモードは iOS 12 までと同じ見え方としてダークモードは別に対応しようと決めました。画像アセットの準備
これまで使っていた天気のアイコン画像は黒で背景は透過になっています。
ライトモードだと白基調なので問題ないのですがダークモードだと
黒基調の背景になるのでダークモード用に白アイコンを用意しました。Xcode 上でライトモード時,ダークモード時に設定される画像アセットを用意可能です。
ライトモードとダークモードの2種類で良いので
Appearances
はAny, Dark
とします。
iOS 13 の端末でライトモードの場合はAny
の画像が,ダークモードの場合はDark
の方の画像が使われます。
そして,iOS 13 未満ではAny
として設定した画像が使われます。
2 つのアセットが必要かもと考えていましたが,だいぶ楽に導入できました。
(img_hoge_light
とimg_hoge_dark
のアセットを用意してコードで書き分けるなどは不要です!)下記のように各モードの場合自動で画像が切り替わります。
ライトモード ダークモード ライトモード時とダークモード時に適用する色
いわゆる白と黒とする場合,背景色などであれば
System Background Color
を,
ラベルなどのテキストのフォントカラーであればLabel Color
を
Storyboard やコードで指定するだけで良いです。
他にも多くのシステムカラーが用意されています。
システムカラーはライトモードとダークモードとで少し色合いが異なっています。中途半端に RGB などの指定色やデフォルト色を使っていると
ダークモードにした際に表示がおかしく見えるわけです。
黒背景に黒ラベルなので何も見えないとか逆もしかり。。。用意されていない色の指定をしたい場合は少しコードを書く必要があります。
FLAT Weather Clock アプリでは背景色は白ではなく,
テーマカラーの半蔵門線パープルよりかなり明るいパープルを採用している部分があります。
そのような単純に白・黒で表現できない場合などが例として挙がります。
(イケてないのでデザイン再考は必須・・・)
ライトモード ダークモード ライトモード,ダークモードを取得するには
UITraitCollection
クラスの
userInterfaceStyle
の値を見ます。
また,ライトモードからダークモードに変更された際に
UIUserInterfaceStyle
が変更されるのでUITraitCollection
クラスの
userInterfaceStyle
の値を見て変更するコードを書く必要があります。
この辺りを考慮しないとライトモードからダークモードに変わった時に自動更新できないです。その対応のため,
UIColor
の extension としてクラスメソッドを用意しました。
一度書いておくとコードで色を指定する際便利なメソッドです。extension UIColor { /// ライトモード時のColorとダークモード時のColorを受けて端末のuserInterfaceStyleの値で適切な方の色を返却 public class func setDynamicColor(light: UIColor, dark: UIColor) -> UIColor { if #available(iOS 13, *) { return UIColor { (traitCollection) -> UIColor in return traitCollection.userInterfaceStyle == .dark ? dark: light } } // iOS 13 未満はライトモード用のColorを返却 return light } }例えば,あるラベルのテキストカラーをライトモードの際は
System Green Color
,
ダークモードの際はSystem Blue Color
としたい場合は下記のように呼べば設定できます。hogeLabel.textColor = UIColor.setDynamicColor(light: .systemGreen, dark: .systemBlue)
Label Color
などは,iOS 13 以上なので Storyboard で指定する際はいいのですが,
コードで指定する際は注意が必要です。// 例 if #available(iOS 13, *) { hogeLabel.textColor = .label } else { hogeLabel.textColor = .black }今回は,不具合がある?と噂されていたため使わなかったのですが,
Color Asset もあるのでそちらで画像のときと同じように,
Any
またはDark
の色を設定することもできますね!Secondary Label Color や Secondary System Background Color の色の使いどころ
基本的に,Storyboard の下地(
self.view
相当)はSystem Background Color
を使っています。Grouped スタイルの TableView はGroup Table View Background Color
という感じに用意された色を充てるようにしています。
System Background Color
などは系統が 4 つあります。
Secondary
,Tertiary
,Quaternary Background Color
です。
(Label Color
も同じ)セッションの動画でも紹介されてました。(下記画像)
左がライトモード,右がダークモードでの色合いですね。(白背景なのでほとんど見えないけど)Implementing Dark Mode on iOS セッション動画 より
これらの使い分けはどんな感じでやればいいんだろう?と思っていましたが,
今回の対応で 1度使う機会がありました。View を重ねるような場合です。
FLAT Weather Clock
アプリでは時計ボタンをタップすることで
時計Viewが画面上に天気画面に重ねて表示されます。
この View の背景色にSecondary Background Color
を用いています。Label も同様で,普通の Label とは意味合いが違うといった場合に使う感じです。
あえてGray Color
使っていた部分をSecondary Label Color
に変更したなどの例があります。
Tertiary
やQuaternary
までは使う機会はあまりないかもですが,
Secondary
はうまく使っていきたいと思いました。対応時に困ったこと
当然対応済みのアプリもなく,この場合はどう実装するのが正解なんだろう?ということは多かったです。
ここではそれ以外で困ったことを何個かピックアップして書きます。
ScrollBar Indicator
の仕様変更と色付け先日 Qiita にも書いたのですが,
ScrollBar
のインジケータに画像を
これまで充てていたのですがこのコード部分でクラッシュしてしまっていました。原因は iOS 12 まではインジケータを
UIImageView
で取得できていたのですが,
iOS 13 からはこちらが UIView に変わっていたため,
UIImageView
として取得できずに nil となりクラッシュとなっていたことでした。
UIView
として取得して,インジケータ用の画像をaddSubView
することで対応します。取得した
UIView
のBackground Color
を設定しても色がおかしいということがあります。
こちらはおそらく Indicator の Style の問題です。
ライトモード,ダークモードでそれぞれ色合いが変わってしまうので,
好きな色を出したい場合は画像としてaddSubView
した方がいいかもなと感じました。
(現行アプリはまだ色がおかしいままなので次バージョンで変更する)詳しくは,
iOS 13 端末で UIScrollView の Indicator に画像を設定するコード部分がクラッシュしていたのでその対応
をご覧ください。NavigationBar のLarge Title の色付け
iOS 13 から Large Title 設定時に
Navigation Bar
の
barTintColor
が効いていないのが気になる。デフォルトの NavigationBar 色なら良いが,
着色する場合はどう表示させるのが正解なのかいまいちわかっていない。
UINavigationBarAppearance
あたりを見て対応する。おわりに
今回は,個人アプリのダークモード対応について書きました。
最初は表示崩れに頭抱えてしまったのですが,いざ対応を始めてみると
ほぇ〜こんな表示になるのかぁと楽しくなってどんどん対応進められました。
また,ダークモードに対応することでそれぞれの画面で謎の満足感が得られました。
Map などとても表示がカッコ良いので各アプリはぜひ対応して欲しいです。今回は,ダークモードの表面だけ的な意識なので,ドキュメントの読み込みや
他の対応したアプリを使ってみて,こういう場合はこうするといいのかーとか
どんどんダークモードに関する知見をためて,活かしていきたいと思います。
今回の対応では扱う機会がなかったので Blur 周りも触ってみたい。皆様も楽しいダークモード対応を!
乱文になりましたが,ご覧いただきありがとうございました。
参考セッション
- 投稿日:2019-10-04T15:52:21+09:00
App StoreのPVを作るスクリプト
こんな人向け
iPhoneX Max持ってない、iPhone8持ってない、iPad Pro持ってない…けど、
iPhoneX(R)、iPhone(6sとかなんでも)、普通のiPadなら持っている人が、AppStoreで必要なPVの動画を作る方法。
※もちろん引き伸ばすので解像度の最適化はされけど、とりあえず用意したい準備
各iPhoneX系、iPad、iPhone6系でそれぞれ、QuickTime Playerで動画を取る。
※INPUTファイル名とOUTPUTファイル名は適宜変更してください。iPhoneX向けffmpeg -i ./iPhoneX.mov -r 30 -vf scale=886:1920 886x1920.moviPhone向けffmpeg -i ./iPhone.mov -r 30 -vf scale=1080:1920 1080x1920.moviPad向けffmpeg -i ./iPad.mov -r 30 -vf scale=1200:1600 1200x1600.mov注意事項
アップロードはSafariで
ムービーの長さは15秒〜30秒で
iPad Pro(第2世代・第3世代)詳しくは
https://help.apple.com/app-store-connect/?lang=ja-jp#/dev4e413fcb8
- 投稿日:2019-10-04T15:03:19+09:00
AWSで構築したWebサイトがiPhoneで開けないときに試すこと
AWS で EC2 + RDS + ELB などで構成したWebサイトをiPhoneで開こうとすると
なぜか「ページを開けません。サーバーが見つかりません。」と表示されて開けない問題に直面しました端末側の問題だと思い、以下のことを実施しましたが解決せず。
- 「ホーム画面」→「設定アプリ」→「スクリーンタイム」→「コンテンツとプライバシーの制限」をオンにし、
「コンテンツとプライバシーの制限」画面にて「コンテンツ制限」を選択し、
「許可されたWebサイトのみ」から「無制限アクセス」を選択します- 「ホーム画面」→「設定アプリ」→「Safari」→「履歴とWebサイトデータを消去」を選択します
- 「ホーム画面」→「設定アプリ」→「Safari」→「Safari検索候補」をオフにします
- 「ホーム画面」→「設定アプリ」→「Safari」→「コンテンツブロッカー」をオフにします
- 端末を再起動します
結果的にこちら( https://qiita.com/ameyamashiro/items/8d4be0f11ffe12472052 )のページに辿り着き、
ELB における HTTP/2 の設定が有効となっているのが原因のようだったので、
HTTP/2 を無効にしたら無事 iPhone からページを表示することが出来ました。設定手順は、
EC2 → ロードバランサー へ遷移し、説明タブの下方にある「属性の編集」ボタンを押し、
HTTP/2 の「有効化」チェックボックスのチェックを外して保存すればOKです。HTTP/2 を使うことで画像、Javascript、CSSなどのファイルを並列処理で受け取れるのでページの表示速度は上がりますが、このような落とし穴もあるので、皆様お気をつけください。
- 投稿日:2019-10-04T12:12:16+09:00
[ERROR ITMS-90060] 正しくないCFBundleShortVersionStringによるApp Store Connectへのアップロードエラー
発生内容と原因
開発しているアプリをApp Store Connectにアップロードする際に、下記エラーが吐かれました。
ERROR ITMS-90060: "This bundle is invalid. The value for key CFBundleShortVersionString '5.0.0-beta.4' in the Info.plist file must be a period-separated list of at most three non-negative integers. Please find more information about CFBundleShortVersionString at https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring"
開発で使っているAlamofireのバージョンが
5.0.0-beta4
であり、CFBundleShortVersionStringが5.0.0-beta4
でした。このため、上記のエラーが吐かれアップロードが正常に終了しない状態となりました。応急処置
Carthageを使っていたので、CheckoutされているAlamofireのInfo.plistで
5.0.0-beta4
から5.0.0
に直接書き換え、$ carthage build --platform iOS --no-use-binaries Alamofireを実行し、再度ビルドしました。
今後の残タスク
Alamofireの正式なver5.0.0がリリースされたのちに、それへの切り替えを行う予定です。
リンク
Alamofire/Info.plist at 5.0.0-beta.4
CFBundleShortVersionStringが
5.0.0-beta4
となっているのが確認できます。
- 投稿日:2019-10-04T12:10:00+09:00
LineSDK入れ込みでハマった話
経緯
- LineSDKでログイン認証することに
- とりあえずSDKを入れて、ID設定して、コード書いてみた
- Unity上だと動作チェックできないのでUnityCloudBuildでiOS, Androidビルド
- >> ビルドエラー! <<
原因1:対応バージョン見てなかった
LineSDKは Android API 17(Android4.2)以上、 iOS 10.0以上対応 です。
これをビルド時の対応バージョン修正することで、Androidはビルドできるようになりました。
だからReadmeは読めとあれほど(ry原因2:Library Search Paths
iOSが相変わらずビルドできない上に、よく見るあのエラーが発生していることを確認。
Undefined symbols for architecture arm64
そう、やつです。
エラー文だけではどこが悪いのか全然分からないやつです。
CocoaPodの設定が悪いのかなど散々迷走したあげく、Swift関連のライブラリが軒並みリンク失敗して入れ込めてないことに気づきました。参考:https://stackoverflow.com/questions/52536380/why-linker-link-static-libraries-with-errors-ios
参考リンク先で言われているように、Unityの
OnPostprocessBuild
内にて、以下を追加することでiOSビルドができるようになりました。
project.AddBuildProperty (target, "LIBRARY_SEARCH_PATHS", "$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)");
感想
今回、UnityCloudBuildを使ったことで、より問題を複雑に考えて迷走しました。
原因はどっちも基本的な部分にあったので、一旦落ち着いてローカル環境でも同じ問題が起きるかなどきちんと確かめていくと絞り込みやすいかなと思いました。
あと、Readmeは読もう。
- 投稿日:2019-10-04T10:04:23+09:00
swift開発備忘録①
概要
swift開発を始めた際に、覚えていたほうが良さそうなものを備忘録としてまとめていきます。
まだまだ駆け出しなので、分かっていないことも多いかと思いますが、参考程度になればと思います。プロジェクト作成時に、デフォルトで生成されるファイル
AppDelegate.swift
アプリが起動した時や、バックグラウンドに行った時などの状態に応じて何らかの処理を書くためのファイル
ViewController.swift
画面に対する処理を書くためのファイル
Main.storyboard
画面の編集をするファイル
Assets.xcassets
アイコンや画像などを管理するフォルダ
LaunchScreen.storyboard
アプリが立ち上がったときにつらっと表示される画面を作るためのファイル
Info.plist
アプリに関する設定をまとめていくためのファイル