20190622のSwiftに関する記事は8件です。

iOS10でもWKWebViewを使ってPOSTしたい

はじめに

初心者のハマりどころのひとつと思われるWebViewにまつわる話です(私もiOS初心者です)。
iOS10でWKWebViewを使用してPOSTでリクエストする時、httpBodyがnilになるというバグがあるようです。
バグについては、iOS10までとiOS11以降で検証されているこちらの記事が分かりやすいです。
解決策はこちらの記事を参考にしました。

前提条件

XCode 10.2.1
Swift4
DeploymentTarget: iOS10

解決案

今回は「はじめに」の参考にした記事で紹介されている、「URLSessionDataTaskで取得したデータをWKWebViewの以下のメソッドに渡す」方法で対応しました。

まずはURLSessionでリクエストを送信する処理を追加していきます。
今回はデータをjson形式にして送ります。
do-catch内でURLSessionDataTaskにリクエストの内容を渡して、リクエストを開始します。

ViewController.swift
    @IBOutlet weak var webViewContainer: UIView!
    private let url = "http://xxx.xxx.x.x"

    override func viewDidLoad() {
        super.viewDidLoad()

        let config = URLSessionConfiguration.default
        let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)

        var request = URLRequest(url: URL(string: url)!)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        let params: [String: String] = [
            "hoge": "hoge",
            "huga": "huga"
        ]

        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: params, options: .prettyPrinted)
            let task = session.dataTask(with: request as URLRequest)
            task.resume()
        } catch {
            print(error.localizedDescription)
        }
    }

URLSessionDataDelegateに、URLSessionで受け取ったデータをWebViewに渡す処理を実装します。UIを更新する処理なのでメインスレッドで行う必要があります。DispatchQueueを使ってメインスレッドでWebViewのロードを行います。

ViewController.swift
extension ViewController: URLSessionDataDelegate {

    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

        DispatchQueue.main.async(execute: {
            let webView = WKWebView(frame: self.webViewContainer.bounds)
            self.webViewContainer.addSubview(webView)
            webView.load(data, mimeType: "text/html", characterEncodingName: "UTF-8", baseURL: URL(string: self.url)!)
        })
    }
}

Web側のソースです。
今回はローカルにXAMPPを入れてテストします。

test.php
<?php
    $json = file_get_contents('php://input');
    echo $json;

結果

以下のようにパラメータが表示されれば成功です。
iPhoneSE_iOS10_0.png

まとめ

iOS13が発表されましたが、iOS10がサポートされている間は、この手法を使うことがあるかもしれません。ただ私はまだiOSの経験が浅いですし、この実装方法で正しいか分かりませんので、もっといい方法があれば知りたいです。

参考

iOS(swift)ガワアプリの作成で色々苦労した話
iOSのWKWebViewの3つのつまずきポイントと解決方法
SwiftでjsonをphpへPOSTする

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

http://www.trendysupplement.com/radiant-swift-keto/

Radiant Swift Keto When you see you have shed pounds, you will be urged to proceed and defeat hindrances in your manner will b simpler. You should have weight reduction responsibility enough information close by the correct reasons and gatherings of help so as to do what you need to do. They have great involvement in this field so they comprehend what it resembles to be overweight and unfortunate. It might likewise create positive reasoning, and a truly necessary adjustment in mentality to assist you with getting to where you need to be. It isn't hard to build digestion in the event that you pursue these straightforward advances.

http://www.trendysupplement.com/radiant-swift-keto/

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

Xcode11で作成したプロジェクトを古いOSに対応させる(とりあえず版)

Xcode11 beta で新規プロジェクトを作成。
001.png

Deployment Targetを古いOSに設定すると、
0002.jpg

エラーがぞろぞろ出てくるのをなんとかしようという話。
004.png


1. AppDelegate.swift と SceneDelegate.swift
エラーが出ているfunctionに@availableを付ける。

AppDelegate.swift    SceneDelegate.swift
@available(iOS 13.0, *)

2. AppDelegateにwindowプロパティを追加する。

AppDelegate.swift
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow? // 追加

とりあえず、これでエラーは出なくなります。

ただし、
(iOS 13)
1. 起動
2. AppDelegate application:didFinishLaunchingWithOptions: (windowはnil)
3. SceneDelegate scene:willConnectTo:options (windowに値がセットされる)

(iOS 12以下)
1. 起動
2. AppDelegate application:didFinishLaunchingWithOptions: (windowに値がセットされる)
3. SceneDelegate 呼ばれない

このような起動時の流れになりますので、windowプロパティにget / setしている場合はもうひと工夫必要になります。


Beta版を元に作成しています。製品版リリースでは変更される可能性があります。

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

http://www.trendysupplement.com/radiant-swift-keto/ ‎

http://www.trendysupplement.com/radiant-swift-keto/
Radiant Swift Keto

The two viewpoints to be center accessible around if your family need coming back to support your wellness parcels and decrease your are oxygen consuming activity wellness and therefore generally speaking dimension. Maintain a strategic distance from pretty much all reasons the reality secures individuals anyplace from losing bottom fat. This utilizes been profited by that numerous individuals keep up profited through these arrangements as individuals represent the reason that source related inspiration. After we get protected you could need in close you're eyes yet additionally unwind.

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

NDxClasses

NDxClasses for macOS, SpriteKit

対象

自分とか

背景

Windows(と、Andorid, PS4, PS Vita, Nintendo Switch)用ゲームライブラリ「DxLib」がiOSに対応したのでmacを調達してビルドして遊んでいるが、そういえばmacOSでは(UnityやUnreal Engine等の本格派ライブラリを除いて)どうやってゲームを作るんだ?と思って調べたところ、Apple純正の2Dゲーム用ライブラリ「SpriteKit」があることを知った。
ちょっと使ってみたのだがなんかこう・・・使いにくい。DxLibのような関数群で組みたいのだ。ちなみに筆者はmacのプログラミングはおろか、Swift言語がまずわかっていない(つか、iPhoneばっかりでmacOSの開発情報は少なすぎんか?)。
まぁ、動いているからおk

  • 本家DXライブラリ (DxLib) はこちら
  • 私がDxLibで作成したアプリはこちら

仕様

物理演算、シーン遷移、当たり判定etc.のSpriteKitが持つメリットは全無視。単に画像を読む、表示する、などの簡易機能を実装していく(ただ、すでに自分の欲しい機能はそろったという話が)。
どうせSwiftではOgg Vorbisの再生もできないし、ちょろっと表示ができればよいのだ。

コード

ちなみにほとんど検証していないので参考程度で。仕様変更した関係でマウス周りは空にしているので注意。
ついさっきも全く正しくなかった部分を直したところ。超適当。

NDxClasses.swift
// Ver. 2019/06/22
import Foundation
import SpriteKit
import GameController // Framework Game Controller
import CoreGraphics //Framework CoreGraphcs
import AVFoundation

class CNDxSound {
    var scene : SKScene
    var filename = ""
    var action : SKAction? = nil

    init(_ Scene : SKScene){ self.scene = Scene }

    func Load(_ filename : String) -> Bool
    {
        if (filename == ""){ return false }
        self.filename = filename
        self.action = SKAction.playSoundFileNamed(self.filename, waitForCompletion: false)

        if (self.action == nil) { return false }
        return true
    }

    func Play() { scene.run(action!) }
}

class CNDxInput
{
    static var controller : GCController? = nil

    static func poll()
    {
        if (GCController.controllers().count > 0)
        {
            controller = GCController.controllers()[0]
        } else {
            controller = nil
        }
    }

    static func Left() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:123)
    }

    static func Right() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:124)
    }

    static func Up() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:126)
    }

    static func Down() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:125)
    }

    static func Enter() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:52)
    }
    // 同じだけどあまぁどっちでも
    static func Return() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:52)
    }
    static func Esc() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:53)
    }
    static func Space() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:49)
    }
    static func Z() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:6)
    }
    static func X() -> Bool {
        return CGEventSource.keyState(CGEventSourceStateID.combinedSessionState, key:7)
    }

    static func Trg_A() -> Bool {
        guard controller != nil else { return false }
        let v = controller?.gamepad?.buttonA.isPressed
        return v!
    }
    static func Trg_B() -> Bool {
        guard controller != nil else { return false }
        let v = controller?.gamepad?.buttonB.isPressed
        return v!
    }
    static func Trg_X() -> Bool {
        guard controller != nil else { return false }
        let v = controller?.gamepad?.buttonX.isPressed
        return v!
    }
    static func Trg_Y() -> Bool {
        guard controller != nil else { return false }
        let v = controller?.gamepad?.buttonY.isPressed
        return v!
    }
}


class NDx {
    private var scene: SKScene
    private var nTouch : Int = 0
    private var mx = 0, my = 0

    private var labels_count : Int
    private var fontSize = 14
    private var fontName = ""
    private var labels: [SKLabelNode] = []
    private var graphs: [cGraph] = []
    private var graphs_dic : [Int : cGraph] = [:]

    private var g_yMAX:Int = 0;
    private var g_alpha:Double = 1.0
    private var zCounter:Int = 0
    private var g_blendColor:NSColor = .white
    private var g_blendColorRate:Double = 0.0

    init(_ Scene: SKScene) {
        scene = Scene
        labels_count = 0
        fontSize = 14
        fontName = ""
        for _ in 0...15
        {
            let node = SKLabelNode()
            labels.append(node)
            node.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.left
            node.verticalAlignmentMode = .top
            node.horizontalAlignmentMode = .left
            scene.addChild(node)
        }
    }

    // DxLibではUInt32だがSpriteKitはNSColor(かつ項目は0-255ではなく0.0-1.0) なのでNSColorに合わせる
    func GetColor(_ R:Int, _ G:Int, _ B:Int, _ A:Int = 255) -> NSColor {
        return NSColor(deviceRed: CGFloat(Double(R) / 255.0), green: CGFloat(Double(G) / 255.0), blue: CGFloat(Double(B) / 255.0), alpha: CGFloat(Double(A) / 255.0))
    }
    // 現在は単なるダミー。DxLibと見た目を揃える目的のみ。なんかないかな
    func ScreenFlip()
    {
    }

    // Ref:  https://qiita.com/yamoridon/items/087e1018565218209eef
    // マイクロ秒単位の時間取得
    func GetNowHiPerformanceCount() -> UInt64 {
        return UInt64(Double(clock_gettime_nsec_np(CLOCK_REALTIME)) / 1000.0)
    }

    // ミリ秒単位の時間取得
    func GetNowCount() -> UInt64 {
        return GetNowHiPerformanceCount() / 1000
    }

    // マウスもリアルタイム取得にしたいのだが、キーボードと違って
    // どこにも書いていない気がする。仕方ないのでイベントで変数をいじる方式にしている
    func GetMouseInput() -> Int { return nTouch }
    func GetMousePoint() -> (Int, Int) { return (mx, my) }

    // 背景色を設定
    func SetBackgroundColor(_ R:Int, _ G:Int, _ B:Int)
    {
        scene.backgroundColor = NSColor.init(red: CGFloat(R) / 255, green: CGFloat(G) / 255, blue: CGFloat(B) / 255, alpha: 1.0)
    }

    // フォントサイズを設定
    func SetFontSize(_ size: Int)
    {
        fontSize = size
    }

    // 画面消去(と見せかけてループごとの各種情報クリア)
    func ClearDrawScreen()
    {
        zCounter = 0
        //viewの次にwindowをつけると外周のサイズになってしまうので注意
        g_yMAX = Int((scene.view?.frame.height)!)
        for i in 0..<labels_count
        {
            labels[i].isHidden = true
        }
        labels_count = 0

        for i in 0..<graphs.count
        {
            for j in 0..<graphs[i].sprites_count
            {
                graphs[i].nodes[j]?.alpha = 1.0
                graphs[i].nodes[j]?.zRotation = CGFloat(0.0) // 表示順(最小0)
                graphs[i].nodes[j]?.anchorPoint = CGPoint(x: 0, y: 0) // 回転の中心位置
                graphs[i].nodes[j]?.xScale = 1.0 // 拡大縮小(マイナス指定で左右反転)
                graphs[i].nodes[j]?.yScale = 1.0
                graphs[i].nodes[j]?.color = NSColor.white // 混ぜる色
                graphs[i].nodes[j]?.colorBlendFactor = 0.0 // 混ぜる割合
                graphs[i].nodes[j]?.isHidden = true
            }
            graphs[i].sprites_count = 0
        }
    }

    // 全画像の破棄
    func InitGraph()
    {
        graphs.removeAll()
        graphs_dic.removeAll()

        labels.removeAll()
        labels_count = 0
    }

    // 画像の破棄
    func DeleteGraph(_ Handle : Int)
    {
        if (Handle < 0) { return }
        guard let data = graphs_dic[Handle] else { return }

        for i in 0..<graphs.count
        {
            if (graphs[i].nodes[0] == data.nodes[0])
            {
                graphs.remove(at: i)
                graphs_dic.removeValue(forKey: Handle)
            }
        }
    }

    // 画像の読み込み
    func LoadGraph(_ fileName: String) -> Int
    {
        let node = cGraph(scene: scene)
        let r : Int = node.Load(fileName)
        if (r > 0)
        {
            graphs.append(node)
            graphs_dic[r] = node
        }
        return r
    }

    // 画像の分割読み込み
    func LoadDivGraph(_ fileName:String, AllNum:Int, XNum:Int, YNum:Int) -> [Int]
    {
        let texture = SKTexture(imageNamed: fileName)
        // ファイル名を間違えても 128x128の ☓ になるんだが、読めなかったと判定するにはどうすりゃいいんだ?
        var retValues: [Int] = []
        if (AllNum <= 0 || XNum <= 0 || YNum <= 0 || (AllNum > XNum * YNum))
        {
            return retValues
        }
        let xsize:Int = Int(texture.textureRect().width)
        let ysize:Int = Int(texture.textureRect().height)

        var count = 0

        let wd0 = CGFloat(xsize) / CGFloat(XNum)
        let ht0 = CGFloat(ysize) / CGFloat(YNum)
        loop1:for y in 0..<YNum
        {
            for x in 0..<XNum
            {
                let node = cGraph(scene: scene)

                let wd = wd0 * CGFloat(x)
                let ht = ht0 * CGFloat(y)
                let rect = CGRect(origin: CGPoint(x: wd, y: ht), size: CGSize(width: wd0, height: ht0))
                let texture2 = SKTexture(rect: rect, in: texture)
                let r : Int = node.Load(texture2)
                if (r > 0)
                {
                    retValues.append(r)
                    graphs.append(node)
                    graphs_dic[r] = node
                }
            }
            count += 1
            if (count+1 == AllNum)
            {
                break loop1
            }
        }

        return retValues
    }

    // 拡大、回転、反転機能付きで画像を表示(アンカーは画像の中心位置になるので注意)
    func DrawRotaGraph( _ x:Int, _ y:Int, _ ExtRate:Double, _ Angle: Double, _ Handle:Int, _ TurnFlagLR:Bool = false, _ TurnFlagUD:Bool = false )
    {
        if (Handle < 0) { return }
        let data : cGraph? = graphs_dic[Handle]
        if (data == nil)
        {
            return
        }
        let n1 = data!.sprites_count
        let n2 = data!.nodes.count
        if (n1 == n2)
        {
            let node = SKSpriteNode(texture: data!.texture)
            data!.nodes.append(node)
            node.isHidden = true
            scene.addChild(node)
        }

        let node = data!.nodes[n1]
        node?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        node?.alpha = CGFloat(g_alpha)
        node?.zRotation = CGFloat(Angle)
        node?.xScale = CGFloat(TurnFlagLR ? -ExtRate: ExtRate)
        node?.yScale = CGFloat(TurnFlagUD ? -ExtRate: ExtRate)
        node?.position = CGPoint(x: x, y: (g_yMAX - 0) - (y + 0 )) // ここはテクスチャの高さを引く必要はない
        node?.zPosition = CGFloat(zCounter)
        node?.color = g_blendColor
        node?.colorBlendFactor = CGFloat(g_blendColorRate)
        node?.isHidden = false
        zCounter += 1
        data!.sprites_count += 1
    }

    // 不透明度を設定
    func SetAlpha(_ alpha: Double)
    {
        self.g_alpha = alpha
    }

    // 画像に混ぜる色と割合を設定
    func SetBlendColor(color:NSColor, blendRate:Double)
    {
        g_blendColor = color
        g_blendColorRate = blendRate
    }

    // 画像を描画
    func DrawGraph(_ x:Int, _ y:Int, _ Handle:Int, _ TurnFlag:Bool = false )
    {
        if (Handle < 0) { return }
        let data : cGraph? = graphs_dic[Handle]
        if (data == nil)
        {
            return
        }
        let n1 = data!.sprites_count
        let n2 = data!.nodes.count
        if (n1 == n2)
        {
            let node = SKSpriteNode(texture: data!.texture)
            data!.nodes.append(node)
            node.isHidden = true
            scene.addChild(node)
        }

        let node = data!.nodes[n1]
        let ht = Int((node?.texture?.size().height)!)
        if (TurnFlag == false)
        {
            node?.xScale = CGFloat(1.0)
            node?.position = CGPoint(x: x, y: (g_yMAX - 0) - (y + ht ))
        }else{
            node?.xScale = CGFloat(-1.0)
            node?.position = CGPoint(x: x + Int((node?.frame.width)!), y: (g_yMAX - 0) - (y + ht ))
        }
        node?.alpha = CGFloat(g_alpha)
        node?.color = g_blendColor
        node?.colorBlendFactor = CGFloat(g_blendColorRate)
        node?.zPosition = CGFloat(zCounter)
        node?.isHidden = false

        zCounter += 1
        data!.sprites_count += 1
    }

    // 文字列の長さを取得
    func GetDrawStringWidth(_ text: String) -> Int
    {
        let n1 = labels_count
        let n2 = labels.count
        if (n1 == n2)
        {
            let node = SKLabelNode()
            labels.append(node)
            node.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.left
            node.verticalAlignmentMode = .top
            node.horizontalAlignmentMode = .left
            scene.addChild(node)
        }

        labels[n1].text = text
        if (labels[n1].fontSize != CGFloat(fontSize))
        {
            labels[n1].fontSize = CGFloat(fontSize)
        }
        if (labels[n1].fontName != fontName)
        {
            // 同じ場合でもセットし直すようで、かなり重い。事前チェックがマスト
            labels[n1].fontName = fontName
        }
        labels[n1].isHidden = false
        labels_count += 1

        let len:Int = Int(labels[n1].frame.width)
        return len
    }

    // 文字列を描画
    func DrawString(_ x:Int, _ y:Int, _ color:NSColor, _ text: String)
    {
        let n1 = labels_count
        let n2 = labels.count
        if (n1 == n2)
        {
            let node = SKLabelNode()
            labels.append(node)
            node.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.left
            node.verticalAlignmentMode = .top
            node.horizontalAlignmentMode = .left
            scene.addChild(node)
        }

        labels[n1].text = text
        labels[n1].fontColor = color
        if (labels[n1].fontSize != CGFloat(fontSize))
        {
            labels[n1].fontSize = CGFloat(fontSize)
        }
        if (labels[n1].fontName != fontName)
        {
            // 同じ場合でもセットし直すようで、かなり重い。事前チェックがマスト
            labels[n1].fontName = fontName
        }

        // 左下が(0,0)なのでプログラムで調整しなければならないのだ(勘弁してくれ
        labels[n1].position = CGPoint(x: x, y: (g_yMAX - 1) - y)
        labels[n1].zPosition = CGFloat(zCounter)
        labels[n1].alpha = CGFloat(g_alpha)
        labels[n1].isHidden = false

        zCounter += 1
        labels_count += 1
    }
}

private class cGraph
{
    static var ID : Int = 100000
    var scene : SKScene
    var nodes: [SKSpriteNode?]
    var texture: SKTexture?
    var sprites_count :Int

    init(scene: SKScene)
    {
        sprites_count = 0
        self.scene = scene
        texture = nil
        nodes = []
    }

    func Load(_ fileName: String) -> Int
    {
        texture = SKTexture(imageNamed: fileName)
        if (texture == nil)
        {
            return -1
        }

        let node = SKSpriteNode(texture: texture)
        nodes.append(node)
        node.anchorPoint = CGPoint(x: 0, y: 0)
        scene.addChild(node)
        node.isHidden = true
        cGraph.ID += 1
        return cGraph.ID - 1
    }

    func Load(_ texture: SKTexture) -> Int
    {
        self.texture = texture
        let node = SKSpriteNode(texture: texture)
        nodes.append(node)
        node.anchorPoint = CGPoint(x: 0, y: 0)
        scene.addChild(node)
        node.isHidden = true
        cGraph.ID += 1
        return cGraph.ID - 1
    }
}

AppDelegate.swiftはそのまんま。
FrameworkとしてCoreGraphicsとGameControllerを追加しておこう。
GameScene.sksに最初からあるラベルは削除しておく。

ViewController.swift
import Cocoa
import SpriteKit
import GameplayKit
import GameController

class ViewController: NSViewController {

    @IBOutlet var skView: SKView!

    // これがないとマウスを動かしてもイベントを拾わない
    override func viewWillAppear() {
        //added in hopes that mouse moved events would be captured
        self.skView.window?.acceptsMouseMovedEvents = true
        self.skView.window?.makeFirstResponder(self.skView.scene)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        if let view = self.skView {
            // Load the SKScene from 'GameScene.sks'
            if let scene = SKScene(fileNamed: "GameScene") {
                // Set the scale mode to scale to fit the window
                scene.scaleMode = .aspectFill

                // Present the scene
                view.presentScene(scene)
            }


            view.ignoresSiblingOrder = true

            view.showsFPS = true
            view.showsNodeCount = true
            }
    }
}

以下は実際に使用する側のソース。

GameScene.swift
import SpriteKit
import GameplayKit
import GameController // Framework Game Controller
import CoreGraphics //Framework CoreGraphics

// Main.storyboardからFull Size Content View のチェックを外す
// View ControllerとViewの両方、及びGameScene.sksのウィンドウサイズを揃える(960x540とか)

class GameScene: SKScene {
    enum GameState { case GS_TITLE; case GS_GAME; case GS_RESULT }
    var gamestate : GameState = GameState.GS_TITLE

    var NDX : NDx! = nil
    var nID : Int = -1, nID2 : Int = -1, nID3: Int = -1

    override func didMove(to view: SKView) {
        // これを記述しておかないとマウスイベント(movedとか)を拾わない
        let options = [NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.activeInKeyWindow] as NSTrackingArea.Options
        let trackingArea = NSTrackingArea(rect:view.frame,options:options,owner:self,userInfo:nil)
        view.addTrackingArea(trackingArea)

        NDX = NDx(self)
        initValues()
    }

    func touchDown(atPoint pos : CGPoint) {   }
    func touchMoved(toPoint pos : CGPoint) {  }
    func touchUp(atPoint pos : CGPoint) {  }
    override func keyDown(with event: NSEvent) {}
    override func mouseDown(with event: NSEvent) {}
    override func mouseDragged(with event: NSEvent) {}
    override func mouseUp(with event: NSEvent) {}
    override func mouseEntered(with event: NSEvent) {}
    override func mouseExited(with event: NSEvent) {}
    override func mouseMoved(with event: NSEvent) {}

    var count:UInt32 = 0
    func initValues()
    {
        var tm : time_t = 0
        srandom(UInt32(time(&tm)))
        gamestate = GameState.GS_TITLE
        NDX.SetBackgroundColor(64, 64, 64)

        var nIDs = NDX.LoadDivGraph("bmp/chiaki.png", AllNum:4, XNum:4, YNum:1)
        nID = nIDs[0]
        nID2 = NDX.LoadGraph("bmp/title.png")
        nID3 = NDX.LoadGraph("bmp/chip1.png")
    }

    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
        switch (gamestate)
        {
        case GameState.GS_TITLE:
            Proc_Title()
            break;
        case GameState.GS_GAME:
            Proc_Game()
            break;
        case GameState.GS_RESULT:
            Proc_Result()
            break;
        }
    }

    func Proc_Title()
    {
        NDX.ClearDrawScreen() // 画面クリア
        NDX.SetFontSize(16) // フォントサイズ設定

        for y in stride(from:0, to:540, by:64)
        {
            for x in stride(from:0, to:960, by:64)
            {
                NDX.DrawGraph(x, y, nID3) // マップチップ
            }
        }

        NDX.DrawGraph(300, 150, nID, true) // さらにキャラ1を左右反転して描画
        let r:Double = Double(count) / 100.0
        NDX.DrawRotaGraph(150, 50, 1.0, r, nID2) // キャラ2を回転し描画
        NDX.SetAlpha(0.3) // 半透明30%
        NDX.DrawRotaGraph(150, 150, 3.0, r, nID2, true, true) // さらにキャラ2を上下左右反転+拡大+回転し描画
        NDX.SetAlpha(1.0) // 不透明に戻す

        NDX.SetBlendColor(color: .red, blendRate: 0.3) // さらにキャラ1に赤色を30%混ぜて描画
        NDX.SetAlpha(0.5)
        NDX.DrawGraph(500, 300, nID)
        NDX.SetAlpha(1.0)
        NDX.SetBlendColor(color: .white, blendRate: 0.0) // 色を戻す

        let s:String = "Count=\(count)"
        let xs = NDX.GetDrawStringWidth(s) // 文字列の長さを取得
        NDX.DrawString(0, 500, .white, s) // 文字の描画
        NDX.DrawString(xs + 16, 500, .green, "rad=\(r)")

        count += 1 // Swiftには count++ という表記方法は無い
        NDX.ScreenFlip() // これは現時点で完全にダミー (DxLibに見た目を合わせるためのみ)
    }

    func Proc_Game()
    {
    }

    func Proc_Result()
    {
    }
}

補足

  • 左下を(0, 0)にしようと提案したヤツとそれを承認したヤツは今すぐ屋上に来い
  • Endキーを押した時の挙動がWindowsとmacOSでは致命的に違うのでイライラ指数がうなぎ登り
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RxSwift入門(ハンズオン)

本日作るもの

よくありそうなログイン画面でIDとパスワードを入れるとログインボタンが活性化し
画面遷移するアプリRx(あり/なし)で作ろうと思います!
スクリーンショット 2019-06-22 11.49.53.png

ますはリポジトリをcloneしてくる

https://github.com/mht-ryo-chiba/RxSwiftSample

次にプロジェクトを開く

FinderからRxSwiftSample.xcworkspace(白いアイコンの方) をダブルクリックして開いてください!
スクリーンショット 2019-06-22 11.54.22.png

まずはビルド!

うまくアプリが起動しない人は教えてください!!!!

画面遷移ができるかを確認

以下を変更して再実行!

ViewController.swift
private func initView() {
        // 初期表示は非活性
        loginButton.isEnabled = false //ここをコメントアウトしてください!
RxSwiftViewController.swift
private func initView() {
        // 初期表示は非活性
        loginButton.isEnabled = false //ここをコメントアウトしてください!

まずは通常パターンのチェック関数を記載

ViewControllerに以下の関数を追加

ViewController.swift
//以下の関数をinitView()の下に追加
// TextFieldの文字数によってログインボタンの状態を変化
private func changeLoginEnabled() {
    if idTextField.text!.count > 0 && passwordTextField.text!.count > 0 {
        print("ボタン活性!")
        // ボタンの活性状態
        loginButton.isEnabled = true
        // ボタンのタイトル色
        loginButton.setTitleColor(UIColor.white,for: UIControl.State.normal) // タイトルの色
        // ボタンのボーダー色
        loginButton.layer.borderColor = UIColor.clear.cgColor
        // ボタンの背景色
        loginButton.layer.backgroundColor = enableBtnColor.cgColor
        // どのくらい角を丸くするか
        loginButton.layer.cornerRadius = 4.0
        // ボーダーのの線の太さ
        loginButton.layer.borderWidth = 2.0
    } else {
        print("ボタン非活性!")
        // ボタンの活性状態
        loginButton.isEnabled = false
        // ボタンのタイトル色
        loginButton.setTitleColor(disenableBtnColor,for: UIControl.State.disabled) // タイトルの色
        // ボタンのボーダー色
        loginButton.layer.borderColor = disenableBtnColor.cgColor
        // ボタンの背景色
        loginButton.layer.backgroundColor = UIColor.clear.cgColor
        // どのくらい角を丸くするか
        loginButton.layer.cornerRadius = 4.0
        // ボーダーのの線の太さ
        loginButton.layer.borderWidth = 2.0
    }
}

次にinitViewの中でloginButtonの記述を消去してchangeLoginEnabledを呼び出し
これで変数に追加した idTextCountpassTextCount の値を1以上にして起動すれば
ボタンが活性状態となり起動時に画面遷移ができるようになります

ViewController.swift
private func initView() {
    changeLoginEnabled()
}

TextFieldの変更検知関数の作成

以下の内容をViewControllerに追加

ViewController.swift
// textFieldの文字数を検知
@objc func checkTextCount(textField: UITextField) {
    print(textField.text!.count)
    // チェック関数の呼び出し
    changeLoginEnabled()
}

initView内でid,passにTextFieldの変更を検知する関数を登録

ViewController.swift
private func initView() {
    changeLoginEnabled()
    // idの入力チェック関数を登録
    idTextField.addTarget(self, action: #selector(ViewController.checkTextCount), for: .editingChanged)

    // passの入力チェック関数を登録
    passwordTextField.addTarget(self, action: #selector(ViewController.checkTextCount), for: .editingChanged)
}

ここまでで入力時のバリデーションは完了となりますが
最後に画面が再描画された際にカウントとtextFieldの初期化を行えば通常画面の入力バリデーションは完成です!
viewDidLoad()の下に以下を追加

ViewController.swift
override func viewWillAppear(_ animated: Bool) {
    // 状態を初期化
    idTextField.text = ""
    passwordTextField.text = ""
    changeLoginEnabled()
}

ViewController.swift全体!

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var loginButton: UIButton!
    @IBOutlet weak var idTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!

    let enableBtnColor: UIColor = UIColor(red: 96/255, green: 163/255, blue: 255/255, alpha: 1.0)
    let disenableBtnColor: UIColor = UIColor(red: 173/255, green: 173/255, blue: 173/255, alpha: 1.0)

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        initView()
    }

    override func viewWillAppear(_ animated: Bool) {
        // 状態を初期化
        idTextField.text = ""
        passwordTextField.text = ""
        changeLoginEnabled()
    }

    private func initView() {
        changeLoginEnabled()

        // idの入力制限を登録
        idTextField.addTarget(self, action: #selector(ViewController.checkTextCount), for: .editingChanged)

        //passの入力制限を登録
        passwordTextField.addTarget(self, action: #selector(ViewController.checkTextCount), for: .editingChanged)
    }

    // TextFieldの文字数によってログインボタンの状態を変化
    private func changeLoginEnabled() {
        if idTextField.text!.count > 0 && passwordTextField.text!.count > 0 {
            print("ボタン活性!")
            // ボタンの活性状態
            loginButton.isEnabled = true
            // ボタンのタイトル色
            loginButton.setTitleColor(UIColor.white,for: UIControl.State.normal)
            // ボタンのボーダー色
            loginButton.layer.borderColor = UIColor.clear.cgColor
            // ボタンの背景色
            loginButton.layer.backgroundColor = enableBtnColor.cgColor
            // どのくらい角を丸くするか
            loginButton.layer.cornerRadius = 4.0
            // ボーダーのの線の太さ
            loginButton.layer.borderWidth = 2.0
        } else {
            print("ボタン非活性!")
            // ボタンの活性状態
            loginButton.isEnabled = false
            // ボタンのタイトル色
            loginButton.setTitleColor(disenableBtnColor,for: UIControl.State.disabled)
            // ボタンのボーダー色
            loginButton.layer.borderColor = disenableBtnColor.cgColor
            // ボタンの背景色
            loginButton.layer.backgroundColor = UIColor.clear.cgColor
            // どのくらい角を丸くするか
            loginButton.layer.cornerRadius = 4.0
            // ボーダーのの線の太さ
            loginButton.layer.borderWidth = 2.0
        }
    }

    // textFieldの文字数を検知
    @objc func checkTextCount(textField: UITextField) {
        print(textField.text!.count)
        // チェック関数の呼び出し
        changeLoginEnabled()
    }

    // 画面遷移用関数
    @IBAction func moveNextView(_ sender: Any) {
        // RxSwiftViewControllerに繊維
        let storyboard: UIStoryboard = UIStoryboard(name: "RxSwift", bundle: nil)
        let viewController: UIViewController = storyboard.instantiateViewController(withIdentifier: "RxSwift")
        viewController.modalTransitionStyle = UIModalTransitionStyle.flipHorizontal
        self.present(viewController, animated: true, completion: nil)
    }

}

Rx側の実装(ここからはRxSwiftViewController.swiftを触ります)

まずはRxを使用するためのImport文を追加

RxSwiftViewController.swift
import RxCocoa
import RxSwift

次に終了を通達する変数を追加

RxSwiftViewController.swift
// passTextFieldの下辺りに追加
private let disposeBag = DisposeBag()

次にViewControllerと同様にチェック関数を追加

RxSwiftViewController.swift
// TextFieldの文字数によってログインボタンの状態を変化
private func changeLoginEnabled() {
    if idTextField.text!.count > 0 && passTextField.text!.count > 0 {
        print("ボタン活性!")
        // ボタンの活性状態
        loginButton.isEnabled = true
        // ボタンのタイトル色
        loginButton.setTitleColor(UIColor.white,for: UIControl.State.normal)
        // ボタンのボーダー色
        loginButton.layer.borderColor = UIColor.clear.cgColor
        // ボタンの背景色
        loginButton.layer.backgroundColor = enableBtnColor.cgColor
        // どのくらい角を丸くするか
        loginButton.layer.cornerRadius = 4.0
        // ボーダーのの線の太さ
        loginButton.layer.borderWidth = 2.0
    } else {
        print("ボタン非活性!")
        // ボタンの活性状態
        loginButton.isEnabled = false
        // ボタンのタイトル色
        loginButton.setTitleColor(disenableBtnColor,for: UIControl.State.disabled)
        // ボタンのボーダー色
        loginButton.layer.borderColor = disenableBtnColor.cgColor
        // ボタンの背景色
        loginButton.layer.backgroundColor = UIColor.clear.cgColor
        // どのくらい角を丸くするか
        loginButton.layer.cornerRadius = 4.0
        // ボーダーのの線の太さ
        loginButton.layer.borderWidth = 2.0
    }
}

initViewの内容もViewControllerと同様に関数呼び出しのみになります

RxSwiftViewController.swift
private func initView() {
    changeLoginEnabled()
}

Textフィールド監視用のメゾットを追加

RxSwiftViewController.swift
// テキストフィールドの変更を監視
func validTxtField(textField: UITextField) {
    // textの変更を検知する
    textField.rx.text.subscribe(onNext: { _ in
        print(textField.text!.count)

        // チェック関数の呼び出し
        self.changeLoginEnabled()
    }).disposed(by: disposeBag)
}

次にinitView内で監視するTextFieldを登録します

RxSwiftViewController.swift
private func initView() {
    changeLoginEnabled()

    // idTextFieldを監視対象に追加
    validTxtField(textField: idTextField)

    // passTextFieldを監視対象に追加
    validTxtField(textField: passTextField)
}

最後に画面を表示されたタイミングでtextFieldを更新するように実装

RxSwiftViewController.swift
override func viewWillAppear(_ animated: Bool) {
    // 状態を初期化
    idTextField.text = ""
    passTextField.text = ""
    changeLoginEnabled()
}

Rx側全体像

RxSwiftViewController.swift
import UIKit
import RxCocoa
import RxSwift

class RxSwiftViewController: UIViewController {

    @IBOutlet weak var loginButton: UIButton!
    @IBOutlet weak var idTextField: UITextField!
    @IBOutlet weak var passTextField: UITextField!

    private let disposeBag = DisposeBag()

    let enableBtnColor: UIColor = UIColor(red: 96/255, green: 163/255, blue: 255/255, alpha: 1.0)
    let disenableBtnColor: UIColor = UIColor(red: 173/255, green: 173/255, blue: 173/255, alpha: 1.0)

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        initView()
    }

    private func initView() {
        changeLoginEnabled()

        // idTextFieldを監視対象に追加
        validTxtField(textField: idTextField)

        // passTextFieldを監視対象に追加
        validTxtField(textField: passTextField)
    }

    override func viewWillAppear(_ animated: Bool) {
        // 状態を初期化
        idTextField.text = ""
        passTextField.text = ""
        changeLoginEnabled()
    }

    // TextFieldの文字数によってログインボタンの状態を変化
    private func changeLoginEnabled() {
        if idTextField.text!.count > 0 && passTextField.text!.count > 0 {
            print("ボタン活性!")
            // ボタンの活性状態
            loginButton.isEnabled = true
            // ボタンのタイトル色
            loginButton.setTitleColor(UIColor.white,for: UIControl.State.normal)
            // ボタンのボーダー色
            loginButton.layer.borderColor = UIColor.clear.cgColor
            // ボタンの背景色
            loginButton.layer.backgroundColor = enableBtnColor.cgColor
            // どのくらい角を丸くするか
            loginButton.layer.cornerRadius = 4.0
            // ボーダーのの線の太さ
            loginButton.layer.borderWidth = 2.0
        } else {
            print("ボタン非活性!")
            // ボタンの活性状態
            loginButton.isEnabled = false
            // ボタンのタイトル色
            loginButton.setTitleColor(disenableBtnColor,for: UIControl.State.disabled)
            // ボタンのボーダー色
            loginButton.layer.borderColor = disenableBtnColor.cgColor
            // ボタンの背景色
            loginButton.layer.backgroundColor = UIColor.clear.cgColor
            // どのくらい角を丸くするか
            loginButton.layer.cornerRadius = 4.0
            // ボーダーのの線の太さ
            loginButton.layer.borderWidth = 2.0
        }
    }

    // テキストフィールドの変更を監視
    func validTxtField(textField: UITextField) {
        // textの変更を検知する
        textField.rx.text.subscribe(onNext: { _ in
            print(textField.text!.count)

            // チェック関数の呼び出し
            self.changeLoginEnabled()
        }).disposed(by: disposeBag)
    }

    // 画面遷移用関数
    @IBAction func moveBeforeView(_ sender: Any) {
        // ViewControllerに繊維
        self.dismiss(animated: true, completion: nil)
    }
}

終わり

今回のハンズオンでは本当に触りだけになりますがRxに触れてみました
Rxは覚えることが多いですがすべてを覚える必要はなく、必要なところを覚えていけばいいので
是非他の使い方を調べて実装等をしてみてください〜?

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

RxSwift入門(座学)

目次

本日のアジェンダ

  • 自己紹介
  • 座学
    • rxとは?
    • rxの歴史
    • 簡単な概念
    • rxのメリットとデメリット
  • ハンズオン!

自己紹介

自己紹介 (1).png

rxとは?

Reactive Extensions(Rx)の略称です
Rx、リアクティブプログラミングなど呼ばれることが多いです

今までのプログラムは主にTransformational programmingと呼ばれるものでした
Transformational programming参考画像
みなさんも見たことがあるかと思いますが上から下にプログラムが流れていくこと
その都度内容、状態を条件によってプログラミング書いていくことが主流でした

リアクティブプログラミングは今までの縦方向に処理が進むのとは違い
変更を伝播させるデータフロー指向のプログラミングパラダイムを指す

・・・・・・・・・・・・・・・・・・・・・・・・・・?
なるほど、わからん?
ということでできる限りわかりやすく簡単に説明しようと思います〜

rxの歴史

そもそもRxの考えがどこから生まれたのかというと
Microsoftが2009年に.Net Framework向けに提供したライブラリでした
しかし、その便利さから様々な言語に移植、提供され始めています

現在はReactiveXというユーザー名で各言語のライブラリが提供されています
https://github.com/ReactiveX

  • Rxjs
  • RxJava
  • RxAndroid
  • RxPY
  • RxSwift
  • RxGo
  • rxdart
  • RxCpp
  • RxKotlin
  • RxPHP

・・・・・・etc
詳しくはリンク先で確認してね?

簡単な概念

なぜここまでRxプログラミングが普及したのか?
それにはRxプログラミングの変更があったデータを伝播させる考え方が大きい理由の一つである

データの変更を伝播させるとは?

わかりやすいもので言うとみんな大好きExcelさんにRxの考えが取り入れられています!
今回はGoogleのスプレッドシートを使用してどういうことか簡単に説明します!

登場人物

Rxにはよく2人の登場人物が出てきます

  • 観察する人
    • 状態が変わったかを見守り続けて変わったときに何かをします
  • 観察される人
    • 状態の変化を持ち続けていて変化があったら観察する人に伝えます

rxのメリットとデメリット

メリット

  • どの言語でもほぼほぼ同じ処理、ロジックが書けること
  • 変更に強い!(そもそも状態の変化に対応するように作られている)
  • Rxを仕事で使う機会が多い(むしろスマホアプリ開発使っていないところが減ってきています)

デメリット

  • 強いて挙げるなら変更しないものに使おうとすれば逆にコストが高くなります・・・・

詳しい資料

詳細説明すると明日になってしまうので以下の資料とかを確認してみてください!
https://coffee-nominagara.com/2018-06-04-150921
https://qiita.com/nakailand/items/8b54dc9b4b39b0809c57

ハンズオン

休憩してハンズオンしますー
https://qiita.com/r_chiba/items/5618a3dd96618222cca8

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

FirebaseだけでiOS版マッチングアプリ「Rose Me」をリリースするまでの話

限定公開した日付になっていたので再投稿しました泣

普段は京都大学で学生をしており、iOSエンジニアとしての歴も3年目となりました。

NoSQLもFirebaseも全く触ったことのなかった僕が、バックエンド全てFirebaseで完結させた知見を残しておきます。

何を作ったのか


ランディングページ
https://app-rose.me/

AppStore
https://apps.apple.com/us/app/rose-me/id1453049174?l=ja&ls=1

恋愛は試着する時代

という信念のもと、試着感覚のデートを提供するマッチングアプリRose Meを開発しました。

空いている時間に、好きな場所1時間だけのデートのおさそいをして、ローズを送ることでデートに行きたいという気持ちを伝えることができます。

デート後に、お互いが気になるボタンを押していれば連絡はとりあうことができ、片方でも押さなければ連絡はとれなくなるといったシステムです。

バラを実際に渡す感じを実装したのでぜひアプリで触ってみてください!

開発チーム

iOSエンジニア2人というめちゃくちゃに偏ったチームでした。

役回りとしては

僕: プロダクト設計・DB設計・Firebase周り全て・アプリ全般・年齢確認Webアプリ・書類全般・UI
相方: 「チャット・プロフィール・マイページ」等の主要画面の開発・LP作成・UI

って感じでした。

後半はZenHubを使用してWeek単位でタスク管理をして開発を進めていました。

ちなみに僕はプロダクト設計・DB設計(NoSQL)・Firebase・Webアプリ(React)は全部初めてでした。

UIは二人とも素人で、これだけを参考にして画面作ってましたww

なぜFirebaseだけでマッチングアプリを作ったのか

iOSエンジニアの友人と法人を立ててマッチングアプリを作ろうということで、2018年の10月から開発をスタートしました。

そこで直面した問題が

「バックエンドできる人いなくね?www」

でした。

「バックエンドを簡単に作れるFirebaseみたいなものがあるらしい。」ぐらいの知識でしたが、Cookpadさんの記事をみて、藁にもすがる思いで速攻採用しました。

結論から言うと、Firebaseを選択して大正解でした!!

使用した技術

Firebase

  • Authentification
    • Facebookログインに使用しました
  • Cloud Firestore
    • NoSQLのデータベース、必須です
  • Cloud Functions
    • DBの変更を監視してプッシュ通知を送ったり
  • Cloud Storage
    • プロフィール画像を保存しています
  • Hosting
    • 年齢確認用のWebアプリをのっけてます

主要なライブラリ

  • Pring: Firestoreでモデルを扱うのにめちゃくちゃ重宝しました。今はdepricatedでBallcapを使うと良いみたいです。
  • HGCircularSlider: Rose Meでは時間を視覚的に扱いたかったので採用しました。
  • RxSwift: 言わずもがな
  • IGListKit: タイムラインの差分更新するのに使用しました。
  • etc...

アーキテクチャ

開発速度を重視したかったのでMVVMを採用しました。

MVVMを触ったことがなくこれもイケるやろ!とおもって採用したら、結局MVCとMVVM足して割る2みたいなコードができあがりました。ホクホク。

苦労したこと

Cloud FunctionsがNode.jsのみ対応

GCP版のCloud Functionsを使えばNode.js・Python・Goに対応していますが、Firebase版ではNode.jsのみです。
これに気づかずTypeScriptを1から勉強して書きました。

ただし僕は、TypeScriptで書くことをオススメします。
動的型付けはかなり辛くて、ドキュメントとエディタの行き来で1日が終わります。

Cloud Functionsをローカルで実行できなかった

Firestoreの変更をトリガーにして処理をするFunctionを書いていました。
そのためデプロイして、Firestoreのフィールドを弄って、ログを見て正しく実行されているかチェックしていました。

このせいで無駄なデプロイを100回はした気がします。

ローカルエミュレータなるものがあるみたいですが、今回は諦めました。
https://firebase.google.com/docs/functions/local-emulator?hl=ja

クライアント側にFirestoreの操作を記述する必要がある

クライアント側でクエリを構成してFirestoreからのフェッチ処理を記述しているので、Android版やWeb版を作る際に通信ロジックをそれぞれに記述しなければならないことが大変。

もしマルチプラットフォームを見据えているのであればCloud FunctionsでREST APIを構成したらいいのかもしれない。

Firestoreの監視による無限ループ

これはあるあるかもしれないですが、Firestoreでの変更を監視して、そのドキュメントに対して何か書き込みをした場合、再びonUpdateが呼ばれ無限に書き込みをしてしまうことがありました。

RxSwiftでは、適切にdistinctUntilChanged()を噛ませないと大変なことになります。(アプリも、請求金額も)

テストデータを手作業で突っ込んでいた

日時を指定してデートを募集するアプリであるため、タイムラインに過去のデートは表示されない仕組みになっています。
(Develop環境は過去のデートを表示しても良かったですが、そこまで頭が回りませんでした)

毎回相方とアプリ内で操作して、募集を作って、ローズを送りあってマッチしてました。

最初の段階でCloud FunctionsでHTTPメソッド叩くだけでテストデータが生成されるような仕組みを作っておいた方がいいです!
絶対に!!

Appleの審査で必ずやりとりをしなければならなかった

合計5回ぐらいAppleに審査を出したんですが、毎回英語でメッセージのやり取りがありました。

毎回テストアカウントを用意して登録フローからレビューしてもらうのですが、

「アプリの全ての機能が使えない。」

と言われました。

というのも、年齢確認が完了していないと投稿や応募ができないため審査ができないとのこと。

やり取りした後も1-2日待たされて、審査に4日はかかります。

テストアカウントだけは年齢確認のフローは飛ばせるようにした方が審査は圧倒的に楽になります!

インターネット異性紹介事業の届出

個人的に何よりも辛かったです。

書類を10個以上用意して警察に持っていくんですが、京都ではインターネット異性紹介事業の届出の前例がなく受理できるかわからないと言われ、3日ほど待たされました:zipper_mouth:
Webアプリじゃないから固定のURLがないのでダメという理由の一点張りで、何を言っても通じませんでした。
奥の方からITに詳しい人が出てきましたが、「GCP...?」「ハッピーメールと一緒?」と言われ悲しい気持ちになりました。

よかったこと

Cloud Firestoreが正式リリースした

2019年2月にFirestoreの正式リリースが行われ東京リージョンができました。(asia-northeast1)
新規開発をする人は、Realtime DatabaseじゃなくてFirestore一択で!!

オンライン/オフラインをあまり気にしなくていい

端末自体にFirestoreのキャッシュを持っているので、キャッシュに対してクエリを実行することができます。

なので、オフライン時に投稿やプロフィールの変更を行ったとしても、次にオンラインになった時に書き込みをしてくれるのでオフラインのハンドリングに神経質にならなくてもよくなりました。

Hostingが簡単すぎる

年齢確認をするために、WebアプリをReactで作成しました。
Buidディレクトリを指定してdeployするだけでhttps://{project-id}.web.appが生成されます。

こんな感じのWebアプリを作りました。

自分たちで地道に1件1件、年齢確認をしていきます。笑

Pringが神

例えばこれだけでメッセージのスキーマが作れます。
開発者の@1amageek さんに感謝しかありません。

Message.swift
import Pring

@objcMembers
class Message: Object {
    dynamic var text: String?
    dynamic var senderRef: Reference<User> = Reference()
}

結局Firebaseはどうなのか

開発スピードとスケーラビリティを重視するのであれば自信を持ってオススメします!

iOSエンジニアしかチームにいなくて、誰もFirebaseを使ったことがなくても全然なんとかなります。

Firebaseとの連携作業も慣れれば10分で終わります。

ひとこと

今日の朝シャワーを浴びながら思ったことを書こうと思います。

技術しか知らなかった僕が去年の6月に会社を立てて、マッチングサービスを出す!
と意気込んでからちょうど1年がたったのかとシャンプーをしながらしみじみと感じていました。

思えばDeNAのインターンに行ってからユーザーのニーズを考え抜いてサービスを作る大事さ、楽しさを知ったなと思ったり。

デザイナーさんを雇うお金もないのでダサいUIだったのですが、たまたま東京で会うことのできたデザイナーさんにしつこく画面を見せてレビューしてもらったり。(あの時は本当にありがとうございます。)

男だけで恋愛バラエティをみてこんなデートいいなぁ〜っていいながら着想を得たり、毎日大学にこもって開発したり。

「マッチングはレッドオーシャンだから難しい。」
「試着できるデートは斬新で面白そう。」
「ネットで出会うなんてあり得ない。」
と色々な意見をもらって、自信がついたり無くしたりを繰り返していました。

走馬灯のように色々思い出す中で、どれだけの人が自分たちのサービスを使ってくれるのか、パートナーが見つかるのか、不安とドキドキでいっぱいです。

Qiitaにそぐわない記事になってしまいそうなので、ここらへんにしておきます。

名前は伏せさせていただきますが、Rose Meを考案するにあたって様々な方にアドバイスを頂き、無事リリースすることができました。
本当にありがとうございます!

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