20191004のiOSに関する記事は13件です。

@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: name

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

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 での時刻取得が最適。
  • ペアリングしようとしても iPhone の設定アプリの Bluetooth画面に M5StickC のデバイス名が現れない。Bluetooth のテストアプリ(BLE Scanner等)では見え、そこで接続すると時刻取得に成功。
  • 一旦成功した後は Bluetooth画面に名前が現れているので、そこで接続操作を行えばよくなる。
  • その後、いろいろ試しているうちに成功した方法でも readValue しても中身が空になる現象が発生。ここで他の M5Stack で試したりしたが問題ない。ふと M5Burner で Erase してみたらその後は正常に値が取得できるようになった。

課題

環境

  • iPhone 6 iOS 12.4.2 (BLE Server)
  • M5StickC (BLE Client)

実行例

  1. M5StickC 側のアプリ実行後、iPhone側の設定アプリのBluetooth画面には現れない
  2. Bluetooth のテストアプリ(今回は BLE Scanner を使用)で Connect
  3. ペアリング要求画面が現れる
  4. 以降はBluetooth画面にも表示されるようになる

1. 2019-10-05-3 BLE Pairing.png 2.2019-10-05-4 BLE Pairing.png
3. 2019-10-05-5 BLE Pairing.png 4.2019-10-05-6 BLE Pairing.png

実行時のシリアルモニタ出力例

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

TFT_Clock.ino 修正版のアナログ時計。
2019-10-05-2 TFT_Clock_M5StickC.jpeg

RTC.ino 修正版のデジタル時計。
2019-10-05-1 RTC_M5StickC.jpeg

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

SkeletonView

よく商用アプリでも使われているのを見るローディングライブラリである「SkeletonView」を使用したので、使い方の備忘録として書き残しておきます。

import SkeletonView
    override 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/

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

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

新しいバージョンが利用可能になったことをユーザにお知らせする - Siren

Siren とは?

アプリの新しいバージョンが利用可能になったことをユーザにお知らせアラートを表示するライブラリです。
本内容は Siren 5.2.1 をもとに記述しています。

skip

シンプルな実装

import Siren 後、Siren.shared.wail() を didFinishLaunchingWithOptions で呼び出すだけで動作します。

AppDelegate.swift
import 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 ユーザにアプリの更新を強制する force
option 今すぐアプリを更新するか、起動時に更新する
どちらかユーザが選択できる
option
skip ・今すぐアプリを更新する
・次回の起動時に更新する
・このバージョンはスキップする
の内からいずれかをユーザが選択できる
skip

releasedForDays

デフォルトでは更新アラートの表示を1日遅らせているそうです。
(showAlertAfterCurrentVersionHasBeenReleasedForDays: 1)
これは全ての地域の App Store CDN でバイナリが利用可能になる時間を待つためです。
通常、6〜24時間かかるため1日遅らせているようです。
showAlertAfterCurrentVersionHasBeenReleasedForDays に 0 を指定すると即時チェックとなりますがアラートが表示されるのに App Store にアップデートがまだ存在しない状況が発生する可能性があります。

Localization

デフォルトではアプリの言語設定に従ってメッセージが表示されます。
設定が存在しない場合は英語で表示されます。
SirenTest_xcodeproj.png
「設定 > 一般 > 言語と地域 > 使用言語」でメッセージが切り替わります。
Simulator Screen Shot - iPhone Xs - 2019-09-20 at 15.25.14.png Simulator Screen Shot - iPhone Xs - 2019-09-20 at 15.22.04.png

言語を強制したい場合

例えばどんな場合でも日本語で表示したい場合は PresentationManager を指定します。

siren.presentationManager = PresentationManager(forceLanguageLocalization: .japanese)

独自の文言で表示したい場合

あらかじめ用意されたメッセージではなくオリジナルな文言を用いたい場合も PresentationManager を設定します。
ボタンの文字色も変更できます。

        siren.presentationManager = PresentationManager(
            alertTintColor: UIColor.orange,
            appName: "アプリ名",
            alertTitle: "新しいバージョンがあります",
            alertMessage: "新バージョンではXXX機能がご利用頂けます",
            updateButtonTitle: "アップデートする",
            nextTimeButtonTitle: "今はしない",
            skipButtonTitle: "次のバージョンまで表示しない",
            forceLanguageLocalization: .japanese)

Simulator Screen Shot - iPhone Xs - 2019-09-20 at 15.50.03.png

オリジナルのメッセージには新しいバージョン番号が表示されていますが、独自の文言を設定する場合には新しいバージョン番号を差し込むことは今のところできません。

iOS13 対応

5.2.1 より古いバージョンでは、iOS13でダイアログが表示されてすぐ画面遷移してしまう不具合が発生していますので 5.2.1 以降へアップデートしましょう。

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

ObjectMapper failed to serialize responseが出たらJSONを疑え

はじめに

APIでJSONを取得してくる時に、静かにエラーになっていた。
ニュースアプリを作っているのですが、記事の筆者の情報が取れていなかった。

環境

pod 'AlamofireObjectMapper', '~> 5.2'
pod 'ObjectMapper', '~> 3.4'
Swift4.2
Xcode11
target? iOS:12.1

エラーと対処法

NetworkUtils.swift
let 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/

ここで、無駄な改行を消したらソースは変更せずに解決しました!

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

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-ios

Reactのモジュールが無いよとビルドエラーになった場合、以下のコマンドで、モジュール取得して再ビルドしたらいけた。

cd ios
pod install

こちら参考

動いたあとは、App.jsを修正していくことでアプリの開発が行える。 簡単

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

個人開発のアプリでダークモード対応したので進め方とその所感

はじめに

ついに iOS 13 がリリースされましたね。
私のリリースしているアプリに関しては対応を済ませて
iOS 13 リリース日の 9/20 ギリギリにリリースしました。

iOS 13 では大きな変更が多数あり,さらに意図しないところでクラッシュしたり,
変更があったりして今までになく対応を強いられる部分も多かったです?
大きな変更のひとつがダークモード対応ですね。
みなさん記事を書かれているので私もダークモード対応の進め方などについて書こうと思います。

キャッチアップ

WWDC 18 で macOS にダークモードが搭載されることが発表され,
いつも通りなら来年は iOS かなと思われていましたが,
やはりiOS 13からダークモード搭載となりました。
(WWDC19のサイトのデザインやもらったアップルロゴの漆黒のピンバッチでも感じられましたね)

IMG_1421.jpg

私はその発表を会場で聞いていてダークモードでの表示にかっけーと感心していました。
早速 Xcode 11 で自分のアプリをビルドしてみて,予想以上の表示崩れに絶望してから
SwiftUI のキャッチアップに移ってしまったので 6月はじめから 8月中旬までほとんど手つかずでした。

お盆が終わって業務でやっている案件が落ち着いてきたので,
個人アプリの iOS 13 対応にようやく取り組めた感じです。
そもそもダークモード対応するか,Xcode 11 でビルドしてみた
アプリをしばらく触ってみつつ,案件でも対応することがあるかもしれないし,
どういう感じかまずはみてみようと決めてキャッチアップを始めました。

ライトモードだけの選択肢

Apple は全アプリがダークモードに対応することを期待してるよ!と言っているのですが,
今すぐに対応しなくてもライトモードで引き続きアプリをリリース出来なくはないです。
info.plistUIUserInterfaceStyle キーを追加して Value を Light にすれば良いです。
逆に追加しない場合は Xcode 11 でビルドするとダークモード対応となってしまうということです。

LightModeOnly.png

ダークモード対応するなら,設定が全画面に及ぶことからデザインは全体的な見直しが必要です。
コードで画面ごとに対応・・・もできますが現実的ではないです。
業務レベルならば,アプリのテーマカラーなどの見直しに始まり,
画像の色あいを変更したり,各画面のデザイン仕様書を作ってから実装を始めるという感じになると思われます。

そして実装したはいいが,画面によっては少しテコ入れをーとか普通にありそうです。
一気に対応をするとなると,デザイナーやクライアントの確認を含め,
画面数に比例して結構な工数を準備して取り組む必要がありそうです。

私のダークモードの対応手順

どんな表示になるかをまず確認する必要があります。
私の場合,まず Xcode 11 で既存のプロジェクトをビルドして実機で確認するところから始めました。
各画面を表示させてみて,対応が必要な画面をリストアップして画面ごとにチケットとしました。

Ticket.png

チケットごとにブランチを切って,マージリクエストを作成する際にどういう表示になるのか,
修正前のスクリーンショットとともに貼って確認しました。
(個人開発なのでレビュアも自分。レビューはやらせですw)

IMG_2579.jpg

今回は個人アプリでの対応なのでそこまで深刻に考えずに,
シンプルに対応してみようと考えました。
ライトモードは iOS 12 までと同じ見え方としてダークモードは別に対応しようと決めました。

画像アセットの準備

これまで使っていた天気のアイコン画像は黒で背景は透過になっています。
ライトモードだと白基調なので問題ないのですがダークモードだと
黒基調の背景になるのでダークモード用に白アイコンを用意しました。

Xcode 上でライトモード時,ダークモード時に設定される画像アセットを用意可能です。

icon_any_dark.png

ライトモードとダークモードの2種類で良いので AppearancesAny, Dark とします。
iOS 13 の端末でライトモードの場合は Any の画像が,ダークモードの場合は Dark の方の画像が使われます。
そして,iOS 13 未満では Any として設定した画像が使われます。
2 つのアセットが必要かもと考えていましたが,だいぶ楽に導入できました。
(img_hoge_lightimg_hoge_dark のアセットを用意してコードで書き分けるなどは不要です!)

下記のように各モードの場合自動で画像が切り替わります。

ライトモード ダークモード
top_light.png top_dark.png

ライトモード時とダークモード時に適用する色

いわゆる白と黒とする場合,背景色などであれば System Background Color を,
ラベルなどのテキストのフォントカラーであれば Label Color
Storyboard やコードで指定するだけで良いです。
他にも多くのシステムカラーが用意されています。
システムカラーはライトモードとダークモードとで少し色合いが異なっています。

systemcolor.png

中途半端に RGB などの指定色やデフォルト色を使っていると
ダークモードにした際に表示がおかしく見えるわけです。
黒背景に黒ラベルなので何も見えないとか逆もしかり。。。

用意されていない色の指定をしたい場合は少しコードを書く必要があります。
FLAT Weather Clock アプリでは背景色は白ではなく,
テーマカラーの半蔵門線パープルよりかなり明るいパープルを採用している部分があります。
そのような単純に白・黒で表現できない場合などが例として挙がります。
(イケてないのでデザイン再考は必須・・・)

ライトモード ダークモード
tab2_light.png tab2_dark.png

ライトモード,ダークモードを取得するには 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 の色を設定することもできますね!

coloraseet.png

Secondary Label Color や Secondary System Background Color の色の使いどころ

基本的に,Storyboard の下地(self.view 相当)は System Background Color を使っています。Grouped スタイルの TableView は Group Table View Background Color という感じに用意された色を充てるようにしています。

System Background Color などは系統が 4 つあります。
SecondaryTertiaryQuaternary Background Color です。
(Label Color も同じ)

セッションの動画でも紹介されてました。(下記画像)
左がライトモード,右がダークモードでの色合いですね。(白背景なのでほとんど見えないけど)

primarytachi.png

Implementing Dark Mode on iOS セッション動画 より

これらの使い分けはどんな感じでやればいいんだろう?と思っていましたが,
今回の対応で 1度使う機会がありました。View を重ねるような場合です。
FLAT Weather Clock アプリでは時計ボタンをタップすることで
時計Viewが画面上に天気画面に重ねて表示されます。
この View の背景色に Secondary Background Color を用いています。

secondary_background_color.png

Label も同様で,普通の Label とは意味合いが違うといった場合に使う感じです。
あえて Gray Color 使っていた部分を Secondary Label Color に変更したなどの例があります。

TertiaryQuaternary までは使う機会はあまりないかもですが,
Secondary はうまく使っていきたいと思いました。

対応時に困ったこと

当然対応済みのアプリもなく,この場合はどう実装するのが正解なんだろう?ということは多かったです。
ここではそれ以外で困ったことを何個かピックアップして書きます。

ScrollBar Indicator の仕様変更と色付け

先日 Qiita にも書いたのですが,ScrollBar のインジケータに画像を
これまで充てていたのですがこのコード部分でクラッシュしてしまっていました。

原因は iOS 12 まではインジケータを UIImageView で取得できていたのですが,
iOS 13 からはこちらが UIView に変わっていたため,
UIImageView として取得できずに nil となりクラッシュとなっていたことでした。
UIView として取得して,インジケータ用の画像を addSubView することで対応します。

取得した UIViewBackground Color を設定しても色がおかしいということがあります。
こちらはおそらく Indicator の Style の問題です。
ライトモード,ダークモードでそれぞれ色合いが変わってしまうので,
好きな色を出したい場合は画像として addSubView した方がいいかもなと感じました。
(現行アプリはまだ色がおかしいままなので次バージョンで変更する)

詳しくは,
iOS 13 端末で UIScrollView の Indicator に画像を設定するコード部分がクラッシュしていたのでその対応
をご覧ください。

NavigationBar のLarge Title の色付け

iOS 13 から Large Title 設定時に Navigation Bar
barTintColor が効いていないのが気になる。

デフォルトの NavigationBar 色なら良いが,
着色する場合はどう表示させるのが正解なのかいまいちわかっていない。
UINavigationBarAppearance あたりを見て対応する。

おわりに

今回は,個人アプリのダークモード対応について書きました。
最初は表示崩れに頭抱えてしまったのですが,いざ対応を始めてみると
ほぇ〜こんな表示になるのかぁと楽しくなってどんどん対応進められました。
また,ダークモードに対応することでそれぞれの画面で謎の満足感が得られました。
Map などとても表示がカッコ良いので各アプリはぜひ対応して欲しいです。

今回は,ダークモードの表面だけ的な意識なので,ドキュメントの読み込みや
他の対応したアプリを使ってみて,こういう場合はこうするといいのかーとか
どんどんダークモードに関する知見をためて,活かしていきたいと思います。
今回の対応では扱う機会がなかったので Blur 周りも触ってみたい。

皆様も楽しいダークモード対応を!

乱文になりましたが,ご覧いただきありがとうございました。

参考セッション

Implementing Dark Mode on iOS

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

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.mov
iPhone向け
ffmpeg -i ./iPhone.mov -r 30 -vf scale=1080:1920 1080x1920.mov
iPad向け
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

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

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などのファイルを並列処理で受け取れるのでページの表示速度は上がりますが、このような落とし穴もあるので、皆様お気をつけください。

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

[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となっているのが確認できます。

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

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は読もう。

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

swift開発備忘録①

概要

swift開発を始めた際に、覚えていたほうが良さそうなものを備忘録としてまとめていきます。
まだまだ駆け出しなので、分かっていないことも多いかと思いますが、参考程度になればと思います。

プロジェクト作成時に、デフォルトで生成されるファイル

AppDelegate.swift

アプリが起動した時や、バックグラウンドに行った時などの状態に応じて何らかの処理を書くためのファイル

ViewController.swift

画面に対する処理を書くためのファイル

Main.storyboard

画面の編集をするファイル

Assets.xcassets

アイコンや画像などを管理するフォルダ

LaunchScreen.storyboard

アプリが立ち上がったときにつらっと表示される画面を作るためのファイル

Info.plist

アプリに関する設定をまとめていくためのファイル

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