20210116のiOSに関する記事は9件です。

写真を読み込んで手書きスケッチ。zoom機能付。作ってみた。

はじめに

写真に手書きスケッチを描きこむツールは、いくつか見つかります。
しかし、ズーム機能のついたツールが見つかりませんでした。
標準のフォトやマークアップは拡大率に限界があって 使いにくかった。
だから、今回作ってみました。
uiもスクリプトで作成してみたので、1ファイルで動きます。

 コードをバンバン書ける方には 怒られそうな程注釈を書き込んでいます。
自分用のメモですが、Python勉強し始めの方には スクリプトの動作が追いやすい
とおもって残しています。
あと、試行錯誤の結果 使っていない関数が少し眠っています。

クラスを始めて使って書いてみました。

環境

ipad + pythonista3

要件

・写真を読み込んで、手書きスケッチを書き込める。
・線の色・太さは 数種類づつ用意した。
・undo機能も実装した。
・拡大縮小(zoomin zoomout)機能の実装。zoomは左上を起点にされます。
・拡大機能の実装に合わせてscrollviewを実装。

苦労した点

・scrollviewの実装方法とスクロールと拡大縮小の連携。
・touchイベントとsxrollviewでタッチイベントの受け取りが競合する。
・undoの実装(手書きラインを配列に保管して、最後のラインを消去。その後 全線を再作画)

コード

epaint.py
#! python3
# 画像読み込んで、拡大機能付スケッチできる
# 
# 
# 20201228 ver000 easypaint作成開始。
# 20201229 ver001 scrollviewに大きな画像を見て表示してスクロールを実装。
# 20201231 ver002 bottonmenu追加。座標など表示追加。
# 20210101 ver003 pathを別クラスで作成。touchイベントは scrollviewとpathで競合する。最前面のみ受け取る。
# 20210101 ver004 btn_lock他改。touch競合させないためにscvとpvのタッチ有効を切り替える+フレームそろえてpvの座標補正。
# 20210101 ver005 path作画に色と幅を反映させた。pathの作画位置は左上に飛ぶまま。loadは何バージョンか前に実装済み。
# 20210102 ver006 path作画がスクロールしても左上に飛ばないように修正。pvにoffsetを流し込む処理をいれた。
# 20210102 ver007 save作成。
# 20210103 ver008 zoom作成。zoom時の中心位置が左上の少し上と変。左上付近で作画すると全面塗りのエラーが出る。
# 20210105 ver009 タイトルに画像サイズと倍率とファイル名用引数又はepaintを表示。path描画おかしいの修正。
# 20210105 ver010 save画像が倍率で2倍化するのを修正。save完了を表示。
# 20210106 ver011b b版。undo実装。しかし、写真3200x2400サイズでは作画が遅い。
# 20210107 ver012c c版。動作速度改善やundoの太さ等初期化されるバグ修正。コードの注記と旧コード整理。
# 20210110 ver013c c版。scroll後のzoomで作画位置ズレるバグ修正。ボタン位置修正。
# 残
# 
# 読み込んだ写真ファイル名の取得は、断念。中間ファイル名しか取得できない。元ファイル名+アルファで保存したい。
# QRコードを埋め込んで、元ファイル名渡すのは断念。QRコードのデコードがカメラ経由しかない。
# 画像にステガノグラフィー埋込で元ファイル名を渡すのは、動作スピードからイマイチ。スレッドを分けて作業する? 
# 
# 

import ui, os, sys, photos, scene
import Image, io
import datetime


def pil2ui(imgIn): # pil(jpg) => ui(PNG) pilとios(ui)の使う画像データは異なるので 変換が必要。今回はpil→uiに変換してる。imgIn=pil
    with io.BytesIO() as bIO: # pilの画像データをiosのuiで使える画像データに変換する。import io必要
        imgIn.save(bIO, 'PNG')
        imgOut = ui.Image.from_data(bIO.getvalue())
    del bIO
    return imgOut

########################################
#手描きを画面に作画。SCROLLVIEWとは競合するので、別classにする必要がある。最前面のみ動く。
class PathView (ui.View):
    def __init__(self):
        #self.frame = frame
        self.flex = 'WH'
        self.frame=(0,40,1000,680)
        self.color = 'red'
        self.path_width = 3
        self.path_color = 'red'

        self.action = None
        self.touch_enabled = True
        self.path = None
        self.paths = []
        self.bpath = None
        self.bpaths= []

        self.scvoffset_x = 0
        self.scvoffset_y = 0
        self.scvrate = 1.0
        self.image = None
        self.base_image = None
        self.image_w = 256
        self.image_h = 256


    def touch_began(self, touch):
        x, y = touch.location
        x2 = (self.scvoffset_x + x)/self.scvrate
        y2 = (self.scvoffset_y + y)/self.scvrate

        print('touch_began x  '+str(x)+' y  '+str(y))
        print('           x2:'+str(x2)+' y2:'+str(y2)+' rate'+str(self.scvrate))
        print('scv offset x'+str(self.scvoffset_x)+' y'+str(self.scvoffset_y))

        self.path = ui.Path()#画面描画用
        self.path.line_width = self.path_width#この一文は幅の初期値設定のみ。固定値でOK。
        self.path.line_join_style = ui.LINE_JOIN_ROUND
        self.path.line_cap_style = ui.LINE_CAP_ROUND
        self.path.line_width = 3#
        self.path.move_to(x, y)

        self.bpath=ui.Path()#画像に描画用
        self.bpath.line_width = self.path_width#この一文は幅の初期値設定のみ。固定値でOK。
        self.bpath.line_join_style = ui.LINE_JOIN_ROUND
        self.bpath.line_cap_style = ui.LINE_CAP_ROUND
        self.bpath.line_width = 0#画面上に描画しない様に幅0とする。
        self.bpath.move_to(x2, y2)


    def touch_moved(self, touch):
        x, y = touch.location
        x2 = (self.scvoffset_x + x)/self.scvrate
        y2 = (self.scvoffset_y + y)/self.scvrate

        #print('touch_moved x ;'+str(x)+' y'+str(y))
        #print('touch_moved x2;'+str(x2)+' y'+str(y2))

        self.path.line_to(x, y)#これ実行しないと画像に描画されない。が、実行すると画面に一時的に描画される。
        self.bpath.line_to(x2,y2)#画面描画用と画像描画用両方実行し、画像描画用を幅0で非表示。描画時に幅を渡す。
        self.set_needs_display()

    def touch_ended(self, touch):
        print('touch_ended')

        #bpathsにpathを保存して、undo実装に利用する。pathは経路のみなので、線色と線幅も記録する。
        self.bpaths.append((self.bpath, (int(self.image_w), int(self.image_h)), self.path_color,int(self.path_width)))
        # viewにactionを設定して、描画の関数を呼ぶ。acionに登録しないと上位クラスの関数path_actionを呼べなかった。
        if callable(self.action): # callableは、関数が実行可能かを調べる。要らないかも?
            self.action(self)#pvのactionに設定しているので、path_actionを呼んでいる。
        # pathを1筆毎に初期化。
        self.path = None
        self.bpath = None
        self.set_needs_display()

    #この関数で、タッチを画面に描画している。
    def draw(self):
        if self.path:
            self.path.stroke()
            self.bpath.stroke()

    #undoを実現する時にline_widthをメインプログラム側でいじれなかったのでpvインスタンスで実行
    def pv_bpath_undo(self):
        print('pv_path_undo')
        bpath = None
        if callable(self.action):
            self.action(self)#pvのactionを呼んでいる。path_action
        bpath = None





########################################
# メインプログラム 
class epaint(ui.View):
    def __init__(self,filename1):
        self.name='easy paint'#viewの名前表示。後程ここに、倍率とxy座標uiを表示する。
        self.btn_w=100#ボタンの幅
        self.btn_h=40 #ボタンの高さ
        self.filename1 = filename1

        self.path_color = 'red'
        self.path_width = int(6)
        self.scvoffset_x = int(0)#元画像に対する描画面左上の原点位置
        self.scvoffset_y = int(0)#元画像に対する描画面左上の原点位置
        self.scvrate = float(1.0)#元画像に対する描画面の倍率
        self.scvrate0= float(1.0)#zoom前の描画倍率保管用
        self.image_w,self.image_h=int(0),int(0)#元画像の幅と高さ
        self.pathimage_x,self.pathimage_y=int(0),int(0)#元画像のパス座標
        self.pathscv_x,pathscv_y=int(0),int(0)#描画面のパス座標
        self.paths = []#pathを配列にして保管する。(undo用)
        self.path=None

        self.base_image = ui.Image.named('test:Peppers')#元画像
        self.image_w,self.image_h = self.base_image.size#元画像の幅と高さpil.sizeメソッド

        w,h = ui.get_screen_size()#画面サイズを取得。xとかクラス名除く部分。
        self.scr_w,self.scr_h = w,h #全画面の幅と高さ

        self.biv = ui.ImageView()#base image view元画像のビュー
        self.biv.frame = (0, 0,self.image_w,self.image_h)#元画像のサイズを取得。左上の座標と右下の座標
        #                                  左上絶対座標x,左上絶対座標y,右下x絶対座標,右下y絶対座標
        # ui座標 左上を原点に右下に向かって座標が増える。
        # scene座標 左下を原点に右上に向かって座標が増える。いわゆる普通のxy座標と同じ。
        #

        self.biv.image = self.base_image
        self.biv.bg_color = 'red'
        self.biv.center = (128,128)#viewの中心をどの座標に表示するかを指定。test画像が256x256なので仮に128とした。
        self.biv.transform = ui.Transform.scale(1,1)#bivに表示する画像の倍率を指定。x方向とy方向で指定。
        #self.biv.image = ui.Image.from_data(pil2ui(photos.pick_image()))#動かない。イメージの形式が違う?
        self.scv = ui.ScrollView()
        #フレーム(実際に表示すrscrollviewの窓サイズ)の幅を宣言
        self.scvframesize=(0,self.btn_h+3,self.scr_w,self.scr_h-self.btn_h-80)
        self.scv.frame = self.scvframesize#(0,self.btn_h+3,self.scr_w,self.scr_h-self.btn_h-80)
        self.scv.content_size = (self.image_w,self.image_h)#元画像を入れる枠のサイズを宣言
        #self.scv.flex='WH' #
        self.scv.scroll_enabled = False#False # スクロールがTrue有効=動く・False無効=停止。デフォルトTrue
        self.scv.touch_enabled = False#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。
        #Trueタッチイベント受け取る。下のviewにタッチ渡さない。

        self.scv.add_subview(self.biv)#scvにbivを入れる事で scrollviewの元画像contentsに代入した。
        self.add_subview(self.scv)#selfにscvを入れる事で表示させるのはscvと宣言した


        self.btnscv = ui.ScrollView()#menu buttonのscrollviewを作成
        self.btnscv.background_color = '#d9dcff'#薄紫色
        self.btnscv.frame = (0,0,self.scr_w,self.btn_h)
        self.btnscv.content_size = (1300,self.btn_h)
        self.btnscv.flex = 'W'#幅方向にのみフィックス
        self.add_subview(self.btnscv)

        #menu buttonを登録。同じ項目の繰り返しは下記の関数config_buttonで作っている。
        #config_button(button, name, frame, title)
        self.scv_btn_lock = ui.Button()
        self.config_button(self.scv_btn_lock, 'btn_lock',(5*self.btn_w,0,self.btn_w,self.btn_h), 'Lock')
        self.scv_btn_load = ui.Button()
        self.config_button(self.scv_btn_load, 'btn_load',(0*self.btn_w,0,self.btn_w,self.btn_h), 'Load')
        self.scv_btn_save = ui.Button()
        self.config_button(self.scv_btn_save, 'btn_save',(1*self.btn_w,0,self.btn_w,self.btn_h), 'Save')
        self.scv_btn_undo = ui.Button()
        self.config_button(self.scv_btn_undo, 'btn_undo',(4*self.btn_w,0,self.btn_w,self.btn_h), 'Undo')
        self.scv_btn_color = ui.Button()
        self.config_button(self.scv_btn_color, 'btn_color',(2*self.btn_w,0,self.btn_w,self.btn_h), 'Color')
        self.scv_btn_path_width = ui.Button()
        self.config_button(self.scv_btn_path_width, 'btn_path_width',(3*self.btn_w,0,self.btn_w,self.btn_h), '-')#線の幅を指定するボタンなので title無
        self.scv_btn_zoomin = ui.Button()
        self.config_button(self.scv_btn_zoomin,'btn_zoomin',(6*self.btn_w,0,self.btn_w,self.btn_h),'Zin')
        self.scv_btn_zoomout = ui.Button()
        self.config_button(self.scv_btn_zoomout,'btn_zoomout',(7*self.btn_w,0,self.btn_w,self.btn_h),'Zout')

        self.lock_switch = 'lock'
        self.colors = ['white', 'grey', 'red', 'green', 'blue', 'cyan', 'magenta', 'yellow']
        self.color_nr = 2    #red 線の初期色を指定
        self.path_widths = [3, 6, 12, 24]
        self.path_w_nr = 1      #6 線の幅が 何pかを指定
        self.scv_btn_color.tint_color = self.colors[self.color_nr]



        #タッチを作画に渡すに必要(画面に作画まで実施)しかし、viewが最手前のみtouchイベントを受け取る。
        #つまり、作画とスクロールビューは両立できない。(最前面を入れ替える事で可能.タッチ有効の切替でも可能)
        self.pv = PathView()#(frame=self.bounds)
        self.pv.frame = self.scvframesize#(0,self.btn_h,self.scr_w,self.scr_h-self.btn_h)
        self.pv.action = self.path_action
        self.pv.color=self.colors[self.color_nr]
        self.pv.path_width = self.path_widths[self.path_w_nr]
        self.pv.scvoffset_x = self.scvoffset_x
        self.pv.scvoffset_y = self.scvoffset_y
        self.pv.scvrate = self.scvrate
        self.add_subview(self.pv)

        self.path_width_change()
        self.image = None
        self.set_btn_actions()#btnにactionを追加。(関数読込後しか登録できないので、後追加とした)
        #
        self.present('fullscreen')


    ###############################
    #btn click no action def
    def btn_lock(self,sender):
        self.logp(sender,'btn_lock')
        #scrollviewの要素修正して、スクロールのロック・解除を
        #self.scv.scroll_enabld = True#False # スクロールがTrue有効=動く・False無効=停止。デフォルトTrue
        if self.lock_switch == 'lock':
            print('btn_lock  to scroll')
            self.lock_switch = 'scroll'
            self.scv.scroll_enabled = True
            self.scv_btn_lock.title = 'scroll'
            #self.pv.send_to_back()#ビューを背面に移動する。(手前のビューが不透明だと見えなくなる)
            self.scv.touch_enabled = True#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。
            self.pv.touch_enabled = False#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。
        else:
            print('btn_lock  to lock')
            self.lock_switch = 'lock'
            self.scv.scroll_enabled = False
            self.scv_btn_lock.title = 'lock'
            #self.scv.send_to_back()#viewを背面に送る。これをすると画面が上面のviewの下に隠れる。
            self.scv.touch_enabled = False#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。
            self.pv.touch_enabled = True#Falseタッチイベントを受け取らない。下のviewにタッチ渡す。

            self.scvoffset_x,self.scvoffset_y = self.scv.content_offset
            self.pv.scvoffset_x = self.scvoffset_x#pvの中に現在のscvoffsetを流し込む。
            self.pv.scvoffset_y = self.scvoffset_y


    def btn_load(self,sender):
        print('btn_load')
        self.scvrate = 1
        self.scvrate0 =1

        self.base_image = ui.Image.from_data(photos.pick_image(raw_data=True))
        self.image_w,self.image_h = self.base_image.size#元画像の幅と高さpil.sizeメソッド

        self.pv.image_w , self.pv.image_h = self.image_w , self.image_h

        self.biv.frame = (0, 0,self.image_w,self.image_h)#元画像のサイズにbivを変更
        self.scv.content_size = (self.image_w,self.image_h)#元画像を入れる枠のサイズを宣言
        w,h = ui.get_screen_size()#画面サイズを取得。xとかクラス名除く部分。
        self.scr_w,self.scr_h = w,h #全画面の幅と高さ

        self.scvrate0 = self.scvrate #*2

        self.biv.image = self.base_image#元画像をbivに代入。

        self.zoom_set(sender)

        #titleに画像サイズと倍率を表示
        self.name ='Size:' + str(int(self.image_w)) + ', ' + str(int(self.image_h))+' rate:'+str(self.scvrate)+' '+self.filename1

        self.set_needs_display()

    def btn_save(self,sender):
        print('btn_save')
        saveimage0 = self.ui2pil(self.biv.image)

        nitiji_now = datetime.datetime.now() # 現在日時の取得
        file_nitiji = nitiji_now.strftime("%Y%m%d_%H%M%S" )#pillowはファイル名に全角は使えない。
        # datetime関数から日時を取り出して、表示する文字の変数に代入。ファイル名にいきなり入れると処理落ちする事がある
        filename = file_nitiji+'.jpg'
        print(str(self.image_w)+' , '+str(self.image_h))
        #pil risize int必須。ANTIALIASは、性能重視で遅い。
        saveimage = saveimage0.resize((int(self.image_w),int(self.image_h)),Image.ANTIALIAS)#for PIL
        #zoom関係なく、xy各々2倍になるので、元のサイズに縮小している。
        saveimage.save(filename, quality=95, optimize=True, progressive=True)#for PIL
        # カメラロールに保存する前にpyのフォルダに画像を保存する必要がある。
        filename2 = 'MemoCamera'+str(self.filename1)+str(file_nitiji)+'Draw.jpg'
        #filename1=nyuuryoubunn クラス呼出時のコンストラクタに追加した引数
        os.rename(filename,filename2)#pillowの代わりに全角ファイル名を作成
        #print('Draw_filename2:',filename2)
        photos.create_image_asset(filename2) # カメラロールへの保存。
        # ファイル名は以前に保存した画像ファイルへのパス。保存名ではない。

        #file deleate
        os.remove(filename2) 
        # pyフォルダに生成されたJPEGを消さないと、最初のファイルのタイムスタンプが
        # 写真のexifデータに継承され続ける為、pyフォルダのjpgは毎回消す。
            #titleに画像サイズと倍率を表示
        self.name ='save success'       

        #self.close() #保存したらスクリプトを閉じたいときに有効化する。


    def btn_undo(self, sender):
        print('btn_undo')
        self.path_undo(sender)

        self.set_needs_display()

    def btn_color(self, sender):
        if self.color_nr < len(self.colors) - 1:
            self.color_nr += 1
        else:
            self.color_nr = 0
        self.scv_btn_color.tint_color = self.colors[self.color_nr]
        self.path_color = self.colors[self.color_nr]
        self.pv.path_color = self.path_color
        self.path_width_change()

    def btn_path_width(self, sender):
        if self.path_w_nr < len(self.path_widths) - 1:
            self.path_w_nr += 1
        else:
            self.path_w_nr = 0
        self.path_width = self.path_widths[self.path_w_nr]
        self.pv.path_width = self.path_width
        self.path_width_change()

    def btn_zoomin(self,sender):
        print('btn_zoomin')
        self.scvrate0= self.scvrate
        self.scvrate = self.scvrate * 2
        self.zoom_set(sender)
        self.logp(sender,'zoomin')


    def btn_zoomout(self,sender):
        print('btn_zoomout')
        self.scvrate0= self.scvrate
        self.scvrate = self.scvrate * 0.5
        self.zoom_set(sender)
        self.logp(sender,'zoomout')


###############################                     
# sub tool

    def pil2ui(imgIn): # pil(jpg) => ui(PNG) pilとios(ui)の使う画像データは異なるので 変換が必要。今回はpil→uiに変換してる。imgIn=pil
        with io.BytesIO() as bIO: # pilの画像データをiosのuiで使える画像データに変換する。import io必要
            imgIn.save(bIO, 'PNG')
            imgOut = ui.Image.from_data(bIO.getvalue())
        del bIO
        return imgOut

    #from pythonista forum
    def ui2pil(self, image):
        mem = io.BytesIO(image.to_png())
        out = Image.open(mem)
        out.load()
        mem.close()
        return out


    # ボタンをまとめて設定する関数。同じ属性の繰り返し登録なので 関数化してる。
    def config_button(self,button,name,frame,title):
        button.name = name
        button.frame = frame
        button.title = title
        button.border_width = 1
        button.corner_radius = 2
        button.border_color = 'blue'
        button.font = ('<system-bold>',25)
        #button.action = name#ボタン押した時の関数はボタン名称と同じにした。
        #しかし、コールする関数を先に読み込む必要があるので、ここでは追加できない。後で別の関数で追加。
        self.btnscv.add_subview(button)#menu buttonのscrollview=btnscvにボタンを元ビューとして代入。

    #ボタンのアクションは、アクションの関数を登録していないと宣言できないので、最後に登録する
    def set_btn_actions(self):
        for subview in self.btnscv.subviews:#ボタンの入っているviewを指定する
            if isinstance(subview, ui.Button):
                subview.action = getattr(self, subview.name)

    # imageviewに、加工後の写真を表示する。
    def imgv_pick():
        imgIn = photos.pick_image()
        imggg = pil2ui(imgIn) # 下記でpil→uiに画像形式変換してる。
        #sender.superview['photo1'].image = imggg # imegeviewのphoto_nowにios(ui)形式画像を渡して、反映。
        return imggg
        # ウィンドウサイズに画像を適当にfitさせてくれる。

##############################
# draw tool
    #path_width buttonの線幅画像を変更する
    def path_width_change(self):
        with ui.ImageContext(self.btn_w, self.btn_h) as ctx:
            ui.set_color(self.colors[self.color_nr])
            path = ui.Path()
            path.line_width = self.path_widths[self.path_w_nr]
            path.line_join_style = ui.LINE_JOIN_ROUND
            path.line_cap_style = ui.LINE_CAP_ROUND
            path.move_to(20,20)
            path.line_to(80,20)
            path.stroke()
            image = ctx.get_image()

            #background_imageは、ボタンの背景画像を入れるuiメソッド
            self.scv_btn_path_width.background_image = image
            #線幅のボタンに線色と線幅を反映した画像を背景画像として入れている。



    #特に意味は無い。
    def layout(self):
        pass

    #bivに座標変換したbpathでbivに作画   
    def path_action(self, sender):
        #path = sender.path
        bpath= sender.bpath#pvなどのインスタンス内init外の属性にアクセスする時に必要。関数にもsender要る。
        # 今回の場合「インスタンス内init外の属性」=self.pv.bpath.line_width .但し、pvのactionに登録無いとエラー。

        old_img = self.biv.image
        width, height = self.image_w,self.image_h       
        #bivのサイズ取得だと、画像縮小時に画像の左上部分が切り取られる為、使えない。
        self.logp(sender,'path_action')

        #pathをイメージに作画している。w hは、作画範囲なのでbase_imgeのサイズ。zoomされるbivサイズではない。
        with ui.ImageContext(width, height) as ctx:
            if old_img:
                old_img.draw()
            self.pv.bpath.line_width = self.path_width#線の幅を反映させる
            ui.set_color(self.path_color)#pathの作画色を反映させる    
            bpath.stroke()
            self.biv.image = ctx.get_image()


    def path_undo(self,sender):
        #bpath= sender.bpath
        last_path_color = self.path_color
        last_path_width = self.path_width

        #bpathsリストの個数をカウントしている。undoで再度全bpathを再作画する為。
        path_count = len(self.pv.bpaths)
        #print(self.pv.bpaths)

        width, height = self.image_w,self.image_h
        #print('w h '+str(width)+' , '+str(height))

        if path_count > 0:
            self.pv.bpaths.pop()#最後の要素を削除する
            path_count -= 1
            self.biv.image = self.base_image # bivの画像をbpath無に戻す。
            for i in range(0, path_count):#bpathを最初から1つ前のbpathまで再作画しなおす。
                    self.path_width = self.pv.bpaths[i][3]#self.path_width#線の幅を反映させる
                    self.path_color = self.pv.bpaths[i][2]#
                    self.pv.bpath = self.pv.bpaths[i][0]
                    self.pv.pv_bpath_undo()#線幅を反映させる為にpv中でbpthを生成させている

            self.path_color = last_path_color
            self.path_width = last_path_width

            self.set_needs_display()        


    def zoom_set(self,sender):
        scvw,scvh = self.scr_w,self.scr_h-self.btn_h-80#scvのサイズを取得
        self.scvoffset_x,self.scvoffset_y = self.scv.content_offset#scvの左上原点のbiv中の位置
        #scv offsetを拡大縮小した時の位置近くに移動させる
        newx=self.scvoffset_x * self.scvrate / self.scvrate0
        newy=self.scvoffset_y * self.scvrate / self.scvrate0        

        self.biv.frame = (0,0,self.image_w*self.scvrate,self.image_h*self.scvrate)
        self.scv.content_size = (self.image_w*self.scvrate,self.image_h*self.scvrate)#元画像を入れる枠サイズ

        self.scvoffset_x,self.scvoffset_y = self.scv.content_offset#scvの左上原点のbiv中の位置        
        self.pv.scvoffset_x = self.scvoffset_x#pvの中に現在のscvoffsetを流し込む。
        self.pv.scvoffset_y = self.scvoffset_y
        self.pv.scvrate = self.scvrate
        self.scv.content_size = (self.image_w*self.scvrate,self.image_h*self.scvrate)#元画像を入れる枠のサイズ

        self.scv.content_offset=(newx,newy)#scv offsetを元の位置に近い位置に移動する。左上基準。
        self.scvoffset_x,self.scvoffset_y = self.scv.content_offset
        self.pv.scvoffset_x = self.scvoffset_x#pvの中に現在のscvoffsetを流し込む。
        self.pv.scvoffset_y = self.scvoffset_y      

        self.logp(sender,'zoom_set')
        #titleに画像サイズと倍率を表示
        self.name ='Size:' + str(int(self.image_w)) + ', ' + str(int(self.image_h))+' rate:'+str(self.scvrate)+' '+self.filename1

    #log print
    def logp(self,sender,memo):
        print(str(memo)+' biv.w'+str(self.biv.width)+' h'+str(self.biv.height)+' : offset'+str(self.scv.content_offset)+' rate'+str(self.scvrate)+' rate0:'+str(self.scvrate0))


    ###############################
#v = epaint()
#v.present('fullscreen')

if __name__ == "__main__":
    epaint('epaint')#filename1を引数にしたい。


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

[Swift] core dataの基本的な実装をまとめてみた

はじめに

core dataについて色々調べ、とりあえず実装できた状態です。
自分なりにまとめてみます。
tableviewcontrollerと、遷移先の項目を追加するview2つを作成します。
core data以外の操作は省略します?‍♂️

実装と解説

1 プロジェクト作成

まずはじめにプロジェクトを立ち上げる際に、User Core Dataを選択します。
すると、ファイル名.xcdatamodeldというファイルが追加されます、

2 tableviewcontrollerと追加用のviewcontrollerの実装

イメージはこんな感じです。
スクリーンショット 2021-01-16 21.04.39.png

Core Dataの設定

①のファイルを選択すると、Core Dataの設定ができます。
②でEntityを追加します。
③が追加されます。Entityは今回データを入れておいて保管する場所になります。
今回はメモ内容の保存に使いますので、Memoという名前にしてみます。
Entityは大文字からの名前をつけるのが無難だそうです。
スクリーンショット 2021-01-16 8.04.43.png
④の+を押すと、⑤のAttributeがが追加できます。これは今回追加するデータになります。
今回はメモの内容を保存するnameをstring型、チェックマークの状態を保存するcheckをBoolean型で設定します。
型はTypeから設定できます。
スクリーンショット 2021-01-16 8.16.40.png
各項目のOptionalのチェックを外しておいてください。
ここで一旦ビルドしておきます。しておかないと後々エラーになるそうです。
スクリーンショット 2021-01-16 8.25.35.png

コード

保存の処理

viewContextは変更などを見にいき、操作もできるメソッドで、それを定数contextに入れる
②そのcontextをMemo(context: context)に入れる事で、Core DataであるMemoの変更や操作ができるようになる。
③それを定数memoに入れたので、memo.attributeの項目を操作できる
④最後にsaveContext()でデータを保存して完了!

import UIKit

class AddViewController: UIViewController {

    @IBOutlet weak var addTextField: UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func addButton(_ sender: Any) {
// persistentContainerは総監督の役割でcore dataに関連するNSManagedObjectContext・
// NS PesistentStoreCoordinator・NSpersistentStoreに指示を出せる。
// 以下のcontextはManagedObjectContextへの参照が含まれている。
// viewContextはNSManagedObjectContext型で、管理されているオブジェクトを操作したり、
// 変更を追跡したりする。
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
// (context:)はモデル内の単一のエンティティを表すNSManagedObject(この場合はMemo)のサブクラスに対してのみ、
// 合法的に呼び出すことができる。この場合、Memoの中で変更があるかとかをcontextで追跡し、
// それを呼び出して定数に入れているイメージかな?ManagedObjectのサブクラスを初期化し、
// 指定されたNSManagedObjectContextに挿入できるようになる。
        let memo = Memo(context: context)
//memoはManagedObjectのサブクラスを初期化し、
//指定されたNSManagedObjectContextに挿入できるようになったので、
//以下のように値などを入れる事ができる。
        memo.name = addTextField.text
        memo.check = false
        // 変更があれば保存する処理
        (UIApplication.shared.delegate as! AppDelegate).saveContext()
        navigationController!.popViewController(animated: true)
    }
}

データを取得して、表示するコード

import UIKit

class MemoTableViewController: UITableViewController {

    // 変数memosを用意して、core dataのMemo型を追加していくためにMemo型に指定する
    var memos : [Memo] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: TableViewCell.reuseIdentifier)
    }

    override func viewWillAppear(_ animated: Bool) {
// データを取得する
        getData()
        tableView.reloadData()
    }

    // MARK: - Table view data source

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of rows
        return memos.count
    }


    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.reuseIdentifier, for: indexPath) as! TableViewCell

        let memo = memos[indexPath.row]
        cell.configure(isCheck: memo.check, name: memo.name!)

        return cell
    }

    func getData() {
        // 変更箇所を見にいく
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        do {
            // あれば取得
            memos = try context.fetch(Memo.fetchRequest())
        }
        catch {
            print("読み込み失敗")
        }
    }
}

cellの内容

import UIKit

class TableViewCell: UITableViewCell {
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var checkImage: UIImageView!

    static let image = UIImage(named: "check")

    static let reuseIdentifier = "Cell1"

    func configure(isCheck: Bool, name: String) {
// checkマークの表示の処理
        checkImage.image = isCheck ? TableViewCell.image : nil
        label.text = name
    }    
}

データの内容を削除

editingStyle内で処理を書きます。
また同じように、viewContextで操作できるようにします。
context.deleteで削除できます。
あとは削除した状態をsaveContext()で保存してあげれはOKです。

override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

        if editingStyle == .delete {
            let memo = memos[indexPath.row]
            context.delete(memo)
            (UIApplication.shared.delegate as! AppDelegate).saveContext()
            do {
                // あれば取得
                memos = try context.fetch(Memo.fetchRequest())
            }
            catch {
                print("読み込み失敗")
            }
        }
        tableView.reloadData()
    }

checkマークの切り替え

!を先頭につける事で値が反転できます。
これをdidSeleceRowAtに書く事で、cellのタップ時に切り替える事ができます。

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let memo = memos[indexPath.row]
        memo.check = !memo.check
        tableView.reloadData()
    }

最後に

まだまだ理解できていないところがありますが、実装段階での理解の手助けになれば幸いです。
間違っている箇所などありましたら遠慮なくご指摘ください。

参考サイト

https://blog.codecamp.jp/programming-iphone-app-development-todo

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

Xcode 12.3 が頻繁にフリーズする問題に対処した

はじめに

数分に1回、Xcode v12.3 がフリーズする状況に陥りました。
フリーズするとレインボーアイコンが出て、数分間操作ができなくなります。
だいぶ辛いです。

環境

  • macOS Catalina v10.15.7
  • Xcode v12.3

対処方法

Apple のDeveloper Forums にXcode freezes after upgrade to 12.3 という質問が挙がっています。その回答を参考にします。

Devices and Simulators を開く

Xcode が応答するまで待つか、Xcode を強制終了させて再度立ち上げます。
Xcode が応答しなくなるまでの間に、メニューで Window -> Devices and Simulators を起動させておきます。

Connect via network のチェックを外す

Devices and Simulators に登録されている全ての実機について、Connect via network のチェックを外します。
(ワイヤレスデバッグが出来なくなるが致し方ない。。)

1.png

Unpair Device を行う

Devices and Simulators に登録されているが、電源をオフにしており現在起動していない端末について。
それら全ての実機について、Unpair Device を行って登録を解除します。
(必要があれば、再度デバイスを登録します。)

2.png

さいごに

筆者の環境では、上記の対応をすることによってXcode のフリーズが起こらなくなりました。
記事執筆時点ではDeveloper Forums のスレッドでは未解決となっています。
次のXcode バージョンで解決されるのでしょうか。

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

モバイルアプリ開発は、Flutter一択なのか?-2021版-

こんにちは! Tetsukick(菊池哲平)です。
2021年1月現在、インドネシア(PT.AQ Business Consulting Indonesia)でモバイルアプリ開発の技術顧問をしております。
iOS歴5年、Flutter歴1年半。個人でもアプリ開発してます。
image.png

本記事は、2020年11月にZennに投稿した記事の改訂版になります。
image.png

本記事の対象読者

  • モバイルアプリ開発者
  • Flutterの今後の可能性を知りたい方
  • モバイルアプリ開発案件を検討中で技術選定をされている方
  • 本記事のタイトルが気になる方

今回インドネシアでモバイル開発の技術顧問をさせていただく中で、Flutterの提案から導入までを実施し、実際に導入に至ることができましたので、その過程で用いた技術的根拠等々をシェアいたします。
実際に非技術者に対しても提案をしましたので、そのままお使いいただくことも可能かと思います。
PPT素材が必要な方いましたらTwitter_@tpi29までDMいただければと思います。
日本語文献や英語文献も参考にしておりますので、少しでもお役に立てばと思います。

最近の更新

2021年版のDeveloperのロードマップが公開されました。

結果からお伝えすると、、
モバイル開発におけるフレームワークは、React Nativeが取り上げられております。

とはいえ僕の記事を読んでもらえば少し分かるかもしれませんが、現状そこまで大差はないと思っております。結局は好きな言語を身につけるのが一番かと。。笑
image.png

ネイティブ開発とクロスプラットフォーム開発

まず、モバイルアプリ開発をするにあたって、大きく2つに分類(ネイティブ開発とクロスプラットフォーム開発)できます。

ネイティブ開発とは

image.png

iOSアプリ、Androidアプリに対してそれぞれ独自の言語を用いて開発する開発手法です。
iOSであれば、Objective-CやSwift。
Androidであれば、AndroidJavaやKotlin。
での開発を指します。

モバイルアプリ開発における最も基本的な開発手法になります。

クロスプラットフォーム開発

image.png
クロスプラットフォーム開発とは、AndroidやiOS等の複数のプラットフォームにまたがって開発を行うことのできる手法になります。
具体的には、XamarinやCordova、ReactNative、Flutter、Outsystems等々、最近では、プログラミングを必要としないノーコード開発ツールもいくつか見受けられるようになりました。

ネイティブ開発 VS クロスプラットフォーム開発

では、まずネイティブ開発とクロスプラットフォーム開発の比較をいたします。
以下の項目で比較いたしますが、各項目の重要度は読者様で配分頂いた上で実際に選定していただければ良いかと思います。
- 開発効率
- 保守・運用
- パフォーマンス
- 端末固有機能
- 最新機能対応
- 最新OS対応

まとめ

結論はこちら。以下、各項目に関して詳細に記載いたします。
ネイティブ開発 VS クロスプラットフォーム開発(まとめ)

開発効率

開発効率においてはクロスプラットフォーム開発に軍配があがります。
ネイティブ開発 VS クロスプラットフォーム開発(開発効率)

人的リソース

ネイテイブ開発

ネイティブ開発は各OSごとの異なる言語(iOS: Swift, Android: java, Kotlin)のエンジニアが必要(通常最低2名)

クロスプラットフォーム開発

一つの言語で開発可能。

開発環境

クロスプラットフォーム開発の多くのプラットフォームでホットリロード機能(コンパイル不要でソースコードの修正をアプリに反映させる機能)を有している。
通常ネイティブ開発では、コンパイルごとに10秒-1分程度の時間がかかるので、その時間を削減することができる。

その他

ネイティブの場合、ソースコードは各OSごとに必要で基本的にロジックをあわせる必要があるが、開発者間での調整コストやソースコード間でずれが生じるリスクが発生する。
これに関してはProtocol Buffers等で極力ロジックを統一する等の対策も可能だが、すべてを補完できるわけではない。

保守・運用

保守・運用面では、クロスプラットフォーム開発に軍配があがります。
ネイティブ開発 VS クロスプラットフォーム開発(保守運用)

人的リソース

ネイテイブ開発

ネイティブ開発は各OSごとの異なる言語(iOS: Swift, Android: java, Kotlin)のエンジニアが必要(通常最低2名)

クロスプラットフォーム開発

一つの言語で開発可能。

不具合・改修リスク

ネイティブ開発では各OSごとにソースコードが存在するため、不具合の発生リスクも増加する。
改修の際は両OSでの対応が必要になる。

パフォーマンス

パフォーマンス面においては、ネイティブ開発に軍配があがる。
image.png

レンダリング(画面描画)パフォーマンス

各プラットフォームにより異なるが、クロスプラットフォームはネイティブUIを呼び出す処理を行うもの、WebViewへのレンダリングを行うもの、独自レンダリングエンジンを使用するもの、等ありますが、基本的にはネイティブのレンダリングパフォーマンスに比較すると劣ります。

以下はパフォーマンスの比較例です。
リストを表示する上でのパフォーマンスを比較しています。
リスト表示サンプル
Android比較表iOS比較表

こちらのサイトから参照

端末固有機能

端末固有機能についてはネイティブ開発に軍配があがる。
ネイティブ開発 VS クロスプラットフォーム開発(端末固有機能)

一般的端末固有機能

カメラ機能、プッシュ通知、GPS、生体認証等の機能はプラグインによりサポートされているため、
クロスプラットフォーム開発においても、懸念することはほとんどない。

特殊な端末固有機能

ウィジェット機能、特殊端末との接続(クレジット端末など)、フィットネスAPI等の連携、センサー関連の使用(加速度センサー等)に関しては、プラットフォームにより異なるが、プラグインが提供されていなかったり、プラグインが不十分な場合があり、自作等での対応が必要になるケースがある

最新機能・最新OS対応

最新機能・最新OS対応においては、ネイティブ開発に軍配があがる。
ネイティブ開発 VS クロスプラットフォーム開発(最新機能)

ネイテイブ開発

最新OSのbeta版や開発用のIDEのbeta版が提供され、リリース前から検証や開発を実施し、備えることが可能。

クロスプラットフォーム開発

リリース後にオープンソース上の開発者間で対応バージョンが追ってリリースされることになり、対応を待つ必要がある。
iOSでもAndroidでも、最新OSのリリースの約一年後には、最新OSに対応していないアプリは新たに申請できなくなる風潮がある。

結論(ネイティブ開発 VS クロスプラットフォーム開発)

アプリの要望や開発予算、人的リソース等の状況により、ネイティブ開発、クロスプラットフォーム開発の選定の大きな要因となる。

アプリの要望

特殊なネイティブ機能や最新機能を使用するアプリでは、ネイティブ開発が好ましい。
一般的な機能にとどまるアプリに関しては、クロスプラットフォーム開発が好ましい。

開発予算、人的リソース

開発予算、人的リソースが豊富な場合は、ネイティブ開発が好ましい。
予算や人的リソースに懸念がある場合は、クロスプラットフォーム開発が好ましい。

クロスプラットフォーム開発

まとめ

image.png

クロスプラットフォーム開発分類

image.png

上記のようにクロスプラットフォーム開発は主に3つの種類に分類されます

ネイテイブUI型

単一言語をネイティブのUIに変換して表示、パフォーマンスは比較的に高いが、変換にかかる時間だけパフォーマンスが若干落ちる。各ネイティブのUIを使用できるため、単一言語で各OSに合わせたUIを作成可能

オリジナルUI型

プラットフォーム固有のレンダリングエンジンを用いているため、ゲームアプリなど自由度の高いグラフィック表現が可能。パフォーマンスは各プラットフォーム次第だが、こちらも比較的高い。Unityもこちらに分類される。

WebUI型

WebView上に描画を行うため、パフォーマンスが低い。Webと同じリソースを活用できるため、Webサイトのアプリ化等に優れている。ネイティブの機能との連携もやや困難

ReactNative VS Flutter

本記事では、私自身の経験があり、クロスプラットフォーム開発でもよく比較されるReactNativeとFlutterの比較を行います。

まとめ

以下の項目で比較をいたします。
- 言語
- 生産性
- UI自由度
- 開発頻度
image.png

言語

言語に関して、ReactNativeで採用されているJavascriptの方が開発者数や情報量などで大いに優勢。

下記のサイトでは、かなりの大差ですね。

ランキング 言語 Share率
3位 Javascript 8.44%
21位 Dart 0.56%

image.png
image.png

こちらのサイト参照

生産性

生産性に関しては、ホットリロード機能はどちらもサポートされているが、インタフェースの作成において、XDから正式に変換プラグインのサポートやアニメーションフォーマットのサポート(RIVE)などインタフェースの開発効率が高い

image.png

UI自由度

UI自由度に関しては、ネイティブコンポーネントだけでなく、独自のUIコンポーネントライブラリを保有するFlutterの方が優れている。

image.png

開発頻度

OSSとして、言語の改良速度がFlutterが非常に活発な状態にあります。
質問投稿サイト(StackOverFlow)の質問数やGoogle検索の数に関してもFlutterが優れている。

image.png
github_flutter
github_reactnative

image.png
Google Trends

image.png
StackOverFlow

結論(クロスプラットフォーム開発比較)

プラットフォームの選定には、開発者の習熟言語、アプリの要望にも起因する。

アプリの要望

既にWebサイト構築済みで、特異な機能のないアプリ開発であれば、Cordova等のWebリソースを使用できる開発が好ましい。
OSごとのネイティブUIの表現を重視する場合は、ReactNativeでの開発が好ましい。
ユーザの要望などUIの変更への対応、開発速度が求められる場合はFlutterでの開発が好ましい。

個人的所見

ここ最近の動向を見ても、ReactNativeは、Facebookのネイティブ化などの流れがあることに反して、Flutterは、各種GoogleのアプリがFlutterに変更されたり、中国系のAlibabaやBaidu,TencentなどがFlutterでの開発をしていたり(Flutter showcase)、そういった面でも信頼ができるのではと思います。

また実際に、私がインドネシアで比較的容易に導入できた背景としては、約9割のスマホユーザがAndroidユーザである背景から、ユーザに広く普及しているUIがMaterial Designであるため、その開発においての優位性があるなども別途決め手の一つにはなっていたかと思います。

参考記事

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

UITextFiledで一発でplaceHolderの色を選択できるExtensionを考えてみた

はじめに

どうもこんにちは、TOSHです。
UITextFieldを使用したときに、textColorはそのままセットできるのに、なぜか、placeHolderの色はそのままセットできないって感じた人ことある人は多いと思います。
ということで、placeHolderについてもTextColorと同じようにセットできるようなExtensionを考えてみました。

通常の方法

通常、placeHolderの色を変更する場合は、

let color: UIColor = ~ここで任意の色を指定する~
textField.attributedPlaceholder = NSAttributedString(
                string: "placeHolderの中の文字",
                attributes: [NSAttributedString.Key.foregroundColor : color])

まあ、別にそんなに大変ではないが、NSAttibutedStringを使うのかといった感じですよね。

Extensionを用いた実装方法

extension UITextField {
    var placeHolderColor: UIColor {
        set {
            self.attributedPlaceholder = NSAttributedString(
                string: self.placeholder ?? "",
                attributes: [NSAttributedString.Key.foregroundColor : newValue])
        }
        get {
            let defaultPlaceHolderGray = UIColor(red: 0, green: 0, blue: 0.0980392, alpha: 0.22)
            guard self.attributedPlaceholder?.length != 0,
                  let placeHolderColor =  self.attributedPlaceholder?.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? UIColor else {
                return defaultPlaceHolderGray
            }
            return placeHolderColor
        }
    }
}

extensionのなかで、Stored Propertyはできないので、Computed Propertyを使用します。
setterは簡単なのですが、意外と苦戦したのは、getter。
Place Holderのなかに文字が入っていない可能性もあるので、self.attributedPlaceholder?.length != 0でガードをする必要があります。あと、あくまで、attributedPlaceholderの1文字目を取っているだけなので、1文字目以降で文字が変わるようなAttributedString出会った場合は、うまく動いていないと思っていいでしょう。
ただ、セットすることはあってもなかなか取ってくることはないと思われるのでこれについてはあまり深く考えなくてもいい気がします。。。

まとめ

実装していて気づいたことは、そもそも、Place Holderの色をセットしたが、中身をセットしないということはないので、textのセットと同時に行うことのできるNSAttributedStringを使用しているののだなと思いました。(Textの場合は、ユーザーが文字を入力するので、初期状態で、色を指定しておいて、Textを指定しないということがメインの使い方)
なので、せっかく作ったはいいんですけど、あまり使用されないようなExtensionだなーという感覚です。

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

メモ イヤホン抜き差し

iOSでAudioKitを利用して音を入力する機能を実装する際、本体とイヤホンのマイクを考慮する必要がある。
本体とマイクとでは、サンプルレートが異なることがあり、本体は44.1kHz、iPhone同梱のLightningイヤホンは48kHzとなる。
抜き差しは、AVAudioSessionRouteChangeNotificationを利用するのではなく、AVAudioEngineConfigurationChangeNotificationをトリガーにして、AKMicrophoneを再インスタンス化しつなげ直し、AVAudioEngineを再開させる(AKManager.engine.start())。
また、AudioKitAVAudioEngineConfigurationChangeNotificationをハンドリングしているので、事前に無効にしておく (AKSettings.enableCategoryChangeHandling = false)。

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

【SwiftUI】Mapビューを使用する

この記事は何?

iOS13では、SwiftUIでMapKitを扱うにはUIKit互換のビューでラップする必要がありました。iOS14で、SwiftUIネイティブなビューとしてMapが実装されたようです。
ここでは、「アップル本社の場所を地図上に表示するビュー」のコードを載せておきました。

環境

  • macOS11.1
  • Xcode12.3
  • Swift5.3

コード

SwiftUIのMapビュー
import SwiftUI
import MapKit

struct MapView: View {
    @State var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.3351, longitude: -122.0088),
        span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02))

    var body: some View {
        Map(coordinateRegion: $region)
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}

プレビュー

スクリーンショット 2021-01-16 13.02.48.png

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

【iOS】Firebaseど素人のFirebase入門 認証編

Firebaseど素人だったので、Firebase学習の為に公式ドキュメントを見て、学びをサンプルアプリという形でアウトプットしました。
まずはAuthentication(認証)に挑戦!

作ったサンプルアプリのデモ

イメージ.GIF

現在使用中のユーザーメールアドレスがラベルに表示されるアプリです。

・メールアドレスとパスワードを使った新規登録(サインアップ)
・登録済みのアカウントの認証(サインイン)
・サインアウト
・匿名認証

この機能の実装を行いました。

まずはFirebaseに登録

Firebase公式ドキュメントに沿って、Firebase登録とFirebase SDKをアプリに追加する。
Firebase を iOS プロジェクトに追加する

アプリ内でFirebaseの初期化を行う

AppDelegate.swiftにFirebaseをインポート

AppDelegate.swift
import Firebase

didFinishLaunchingWithOptions内でFirebaseApp.configure()を宣言

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        FirebaseApp.configure()
        return true
    }

使用するログインプロバイダのステータスを変更する

今回はメール/パスワード認証と匿名認証をログインに使用するので、Firebaseに登録したアプリのプロジェクト内AuthenticationSign-in method
メール/パスワード
匿名
こちらの二つのステータスを有効に変更します。
スクリーンショット 2021-01-15 15.43.08.png

ViewControllerにFirebaseをインポート

作成を進めるファイルにもFirebaseをインポートする。

AuthViewController.swift
import Firebase

メール/パスワード認証

新規登録

新規登録にはcreateUser(withEmail:, password:, completion:)を使用します。

Auth.auth().createUser(withEmail: email, password: password) { (authResult, error) in
     guard let user = authResult?.user, error == nil else {
          print("登録に失敗しました:" ,error!.localizedDescription)
          return
     }
     print("登録に成功しました", user.email!)
 }

サインイン

サインインには、signIn(withEmail:, password:, completion:)を使用します。

 Auth.auth().signIn(withEmail: email, password: password) { (authResult, error) in
     guard let user = authResult?.user, error == nil else {
          print("サインインに失敗しました:" ,error!.localizedDescription)
          return
      }
      print("サインインに成功しました", user.email!)
    } 

匿名ログイン

匿名ログインにはAuth.auth().signInAnonymously(completion:)を使います。

Auth.auth().signInAnonymously { (authResult, error) in
    guard let user = authResult?.user, error == nil else {
         print("匿名サインインに失敗しました:" ,error!.localizedDescription)
         return
     }
     print("匿名サインインに成功しました", user.uid)
  }

サインアウト

signOut()を呼ぶだけです。

 do {
     try Auth.auth().signOut()
     } catch let signOutError as NSError {
         print("サインアウトに失敗しました:", signOutError)
     }

現在ログインしているユーザーを取得する

現在ログインしているユーザーを取得するには、Auth オブジェクトでリスナーを設定することをおすすめします。

とのことで、リスナーを設定してみました。

1.AuthStateDidChangeListenerHandleをグローバル変数として定義

AuthViewController.swift
    var authHandle: AuthStateDidChangeListenerHandle?

2.viewWillAppearaddStateDidChangeListener(listener:)をアタッチ

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        authHandle = Auth.auth().addStateDidChangeListener({ (auth, user) in
        //ログイン状態が変更された時の処理を書く
        })
    }

viewWillAppearでアタッチしたこのリスナーは、ユーザーのログイン状態が変更される度に呼び出されます。
変更される度に実行したい処理をここに書きます。
とても便利ですね。
ログインされてるなら別ページに遷移、ログイン無しなら登録ページを表示などの使い分けにも有効そうです。

その他の方法では、currentUserでログインユーザーの状態も取得出来るようです。

currentUser を使用することでも、現在ログインしているユーザーを取得できます。ユーザーがログインしていない場合、currentUser は nil です。

3.viewWillDisappearでリスナーを切り離します。

override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        Auth.auth().removeStateDidChangeListener(authHandle!)
    }

まとめ

Firebaseの公式ドキュメントはとても充実しており、日本語ページもあるので簡単にここまでは進めれました。
もっとしっかり公式ドキュメントを読み、理解を深めていきたいと思います。
何か間違いがありましたら、優しく指摘していただけると幸いです?‍♂️

参考

iOS で Firebase Authentication を使ってみる

サンプリアプリのコード全体

import UIKit
import Firebase

class AuthViewController: UIViewController {

    @IBOutlet weak var emailTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var currentUserEmailLabel: UILabel!

    @IBOutlet weak var signUpButton: UIButton!
    @IBOutlet weak var signInButton: UIButton!

    var authHandle: AuthStateDidChangeListenerHandle?

    override func viewDidLoad() {
        super.viewDidLoad()

        signUpButton.layer.cornerRadius = 22
        signInButton.layer.cornerRadius = 22
        emailTextField.delegate = self
        passwordTextField.delegate = self
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        authHandle = Auth.auth().addStateDidChangeListener({ (auth, user) in
            if let currentUser = user {
                //もし、ユーザーが匿名で利用していたら
                if currentUser.isAnonymous {
                    self.currentUserEmailLabel.text = "匿名で利用中"
                } else {
                    self.currentUserEmailLabel.text = currentUser.email
                }
            } else {
                //ログインをしていない場合
                self.currentUserEmailLabel.text = "現在ログイン中のユーザーはいません"
            }
        })
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        Auth.auth().removeStateDidChangeListener(authHandle!)
    }


    // MARK: - Button Actions

    //サインアウト
    @IBAction func didTapSignOut(_ sender: UIBarButtonItem) {
        do {
            try Auth.auth().signOut()
        } catch let signOutError as NSError {
            print("サインアウトに失敗しました:", signOutError)
        }
    }

    //サインアップ
    @IBAction func didTapSignUp(_ sender: UIButton) {

        if let email = emailTextField.text, let password = passwordTextField.text {
            Auth.auth().createUser(withEmail: email, password: password) { (authResult, error) in
                guard let user = authResult?.user, error == nil else {
                    print("登録に失敗しました:" ,error!.localizedDescription)
                    return
                }
                print("登録に成功しました", user.email!)
            }
        }
    }

    //サインイン
    @IBAction func didTapSignIn(_ sender: UIButton) {

        if let email = emailTextField.text, let password = passwordTextField.text {
            Auth.auth().signIn(withEmail: email, password: password) { (authResult, error) in
                guard let user = authResult?.user, error == nil else {
                    print("サインインに失敗しました:" ,error!.localizedDescription)
                    return
                }
                print("サインインに成功しました", user.email!)
            }
        }
    }

    //匿名サインイン
    @IBAction func didTapSignInAnonymously(_ sender: UIButton) {
        Auth.auth().signInAnonymously { (authResult, error) in
            guard let user = authResult?.user, error == nil else {
                print("匿名サインインに失敗しました:" ,error!.localizedDescription)
                return
            }
            print("匿名サインインに成功しました", user.uid)
        }
    }
}

// MARK: - TextField Delegate Methods

extension LoginViewController: UITextFieldDelegate {

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JUCE: AirDropでファイルを受けられるようにする

問題

JUCEにはContentSharerというクラスがあり、テキストやファイルを他のデバイスにシェアできる機能があるのだが、受けることができない。これはアホなのでなんとかしたい。目標はAirDropでファイルを送って受けられるようにすること。送る側はContentSharerを使うだけなので割愛。

方法

JUCEのソースをいじらないと流石にできなかった。iOS/Macのコードに手を入れる。このメソッドを追加。ring2 changeで括ってあるところが変更点。自信がないのでまどろっこしく書いているがもっと短く書いて良いと思う。

modules/juce_gui_basics/native/juce_ios_Windowing.mm
#if defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0
- (void) userNotificationCenter: (UNUserNotificationCenter*) center willPresentNotification: (UNNotification*) notification
          withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler;
- (void) userNotificationCenter: (UNUserNotificationCenter*) center didReceiveNotificationResponse: (UNNotificationResponse*) response
          withCompletionHandler: (void(^)())completionHandler;

//  ring2 change-->
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options;
//  <-- ring2 change

#endif
#endif

@end

@implementation JuceAppStartupDelegate

    NSObject* _pushNotificationsDelegate;

//  ring2 change-->
- (BOOL)application:(UIApplication *)app
            openURL:(NSURL *)url
            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
{
    auto* juceApp = JUCEApplicationBase::getInstance();
    NSString* absoluteString = [url absoluteString];
    const char* cstr = [absoluteString UTF8String];
    URL juceUrl(cstr);
    BOOL result = juceApp->openUrl(juceUrl);
    return result;
}
//  <-- ring2 change

- (id) init
{
    self = [super init];
    appSuspendTask = UIBackgroundTaskInvalid;

   #if JUCE_PUSH_NOTIFICATIONS && defined (__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0

次に、ApplicationBaseにメソッド1つ追加。

modules/juce_events/messages/juce_ApplicationBase.h
    /** This method is called when the application is being woken from background mode
        by the operating system.
    */
    virtual void resumed() = 0;

    //  ring2 change-->
    virtual bool openUrl(URL& url) { return false; }
    //  <--ring2 change

    /** If any unhandled exceptions make it through to the message dispatch loop, this
        callback will be triggered, in case you want to log them or do some other
        type of error-handling.

        If the type of exception is derived from the std::exception class, the pointer
        passed-in will be valid. If the exception is of unknown type, this pointer
        will be null.
    */
    virtual void unhandledException (const std::exception*,
                                     const String& sourceFilename,
                                     int lineNumber) = 0;



Projucerを使っている場合は、プロジェクトの設定でPreprocessor DefinitionsにJUCE_USE_CUSTOM_PLUGIN_STANDALONE_APP=1を書いて、modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterApp.cppをコピーしたMyStandAloneFilterApp.cppのようなものをプロジェクトに追加し、openUrl()をオーバーライドすれば良い。プラグインのスタンドアロン版の場合はこんなふうにすればOK。自分のプラグインのクラスのヘッダをincludeして。

Source/MyStandAloneApp.cpp
    bool openUrl(URL& url) override
    {
        StandalonePluginHolder* holder = StandalonePluginHolder::getInstance();
        jassert(holder != nullptr);
        IfwAudioProcessor* processor = (IfwAudioProcessor*)holder->processor.get();
        jassert(processor != nullptr);
        processor->openUrl(url);
        return false;
    }

Xcodeでファイルタイプの設定が必要なので、ProjucerのiOSのExporterでCustom Plistに下記のように書いておけば、Info.plistに書き込まれる。UTExportedTypeDeclarationsが適切に記述されていないとopenUrlが呼ばれない模様。記述が足りなかったりすると、iOS 12では呼ばれるがiOS 13以降では呼ばれないとかいうことがあった。

<plist>
    <key>UTExportedTypeDeclarations</key>
    <array>
        <dict>
            <key>UTTypeConformsTo</key>
            <array>
                <string>public.data</string> <-- public.xmlとかpublic.textとかデータに合わせて -->
            </array>
            <key>UTTypeDescription</key>
            <string>My Data</string>
            <key>UTTypeIconFile</key>
            <string>Icon.icns</string>
            <key>UTTypeIconFiles</key>
            <array/>
            <key>UTTypeIdentifier</key>
            <string>com.myurl.myapp.mydata</string>
            <key>UTTypeTagSpecification</key>
            <dict>
                <key>public.filename-extension</key>
                <array>
                    <string>mydata</string>
                </array>
            </dict>
        </dict>
    </array>

    <key>CFBundleDocumentTypes</key>
    <array>
      <dict>
        <key>CFBundleTypeExtensions</key>
        <array>
          <string>mydata</string>
        </array>
        <key>CFBundleTypeName</key>
        <string>My App</string>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>LSItemContentTypes</key>
        <array>
          <string>com.myurl.myapp.mydata</string>
        </array>
        <key>LSTypeIsPackage</key>
        <false/>
        <key>NSPersistentStoreTypeKey</key>
        <string>Binary</string>
      </dict>
    </array>

    <key>LSSupportsOpeningDocumentsInPlace</key>
    <true/>

  </dict>
</plist>


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