Qiita:1分で読める プロトタイピングツール AdobeXD のすすめ!

  • こんばんは!小村だよ!

  • とある理由で限定公開のQiita記事を書いたので、こっちにも書いちゃいます!

  • Qiita Slide用に書いたのをそのまま張り付けてるので読みにくいのはごめんね!




目次

  • プロトタイピングツールって何?
  • Adobe XDの強み
  • 類似ツールとの比較
  • インストール方法
  • 最初にやるべきこと
  • プロトタイプを作るには

プロトタイピングツールって何?

  • めちゃ簡単にプロトタイプが作れる!
    • ↓の画面イメージを10分で作成可能!

image.png


Adobe XDの強み


類似ツールとの比較


インストール方法


最初にやるべきこと

  • スターターキットの初級編はやろう!
  • 1時間未満で主な使い方の9割がわかる!

プロトタイプを作るには


よいプロトタイプ生活を!

調査:モックアップツールの技術選定

  • こんばんは、小村だよ!

  • 前回にひきつづき調査だよ!

  • ちょっとしたら、少人数で集まってアプリのモックを作成することになりそう

  • ひとまずはGoogleSlide使ってつくることになりそうなんだけど

  • いやこのご時世にスライドでモックアップって……

  • 個人的には物足りないので、いいツール探して提案した~~~い!

  • というわけでモックアップツールについて調べていきます!



目次

  1. こだわるポイント
  2. 条件に合うモックアップツール一覧
  3. 決定
  4. XDの使い方を改めて学ぶ



記録

こだわるポイント

  • モックアップツールを選定するうえで、下記の条件を満たすものを探します

    • 前提

      • ひとまずチームは3~5人を想定
    • 必須機能

      • 値段が無料、または安くてある程度使える
    • プラス要素

      • ある程度見栄えがよい
      • チームで共有するのがたやすい
      • 使い方がわかりやすい



条件に合うモックアップツール一覧


cacoo(カコー)

  • 月660円/アカウント

  • 無料期間あり:14日間

  • シンプルで使い方はわかりやすそう

  • 画面遷移図はない

  • チームで使用するという機能は充実していそう


prott(プロット)

  • 月11100円/3アカウント

  • 無料期間あり:2週間

  • 画面遷移図機能がある(モックの完成度が高い)

  • チームで使用するという機能は充実していそう


Adobe XD



決定

  • や、もともとXD使ってたんだけど

  • 改めてほかのとバーッと比べてもさ

  • めっちゃいいじゃんね!

  • 無料であんだけ使えて、1名までは共有もできるとか破格すぎません???

  • たいていのモックアップツールは無料期間あるのに無期限て!!

  • Scetchとかはmacじゃないと使えないけど、こっちはWindowsで使えるし!

  • XDでいいじゃんね!!!!

  • 決定!!!文句は聞かん!!!



XDの使い方を改めて学ぶ

f:id:kom314_prog:20210911163308p:plain


  • 遊びました!初級編は全部やってみたよ!

  • 中級編は途中までやってあと通読して完了!

  • やっぱ直感的でいいですね!AdobeXD!

  • よかったーーー!これで自信をもってADOBE XDを自信をもって人に勧められます!



おわりに

  • 一通りモックの作り方もちゃんと理解できたので、先導してプロトタイプ作れそう!

  • まぁデザイン自体は苦手なんですがね。。そこは他の人に託しちゃお

  • ではでは、ちゃお~~~!



調査:モバイルアプリ開発を作成するうえでの技術選定

  • こんばんは、小村だよ!

  • いつかやりたいけど、まだまだ先だなー、他のこと優先しよ!

  • と考えていたモバイルアプリの作成ですが、なんか急遽やることになりそう!

  • 数か月後には私主導でMVP(Minimum Viable Product)を作成することになりそう!

  • やばい!!!!!!!やったことない!!!!!なんも知らない!!!

  • ということで今日は急遽、技術選定をしていくことにします!!!

  • 調査前の予想としては、flutterかreact nativeだけど、どうなるかな~~~



目次

  1. モバイルアプリの人気言語/フレームワーク調査
  2. モバイルWEBアプリ
  3. モバイルネイティブアプリ
  4. 所感
  5. Swiftかflutterか
  6. 結論



記録

モバイルネイティブアプリの人気言語/フレームワーク調査


モバイルWEBアプリ

メリット

  • OSアップデートの影響をほぼ受けない
  • プラットフォームの審査不要
  • WEBで集客できるので集客単価が低い


デメリット

  • リアルタイムに弱い


言語/フレームワーク


所感

  • Webアプリなら新たな学習が必要あまりなくてうれしいのですが

  • リアルタイムに弱いのは痛すぎますね……

  • カメラ機能や通知機能はおそらく使うからなぁ……



モバイルネイティブアプリ

メリット

  • リアルタイムな対話が可能
  • LTV(顧客生涯価値)向上


デメリット

  • ストア審査や手数料
  • OSアップデートの影響
  • iOSAndroidで別の言語なので工数がかさむ
  • ストアからインストールしてもらう必要があるのでユーザーに手間がかかる
  • 集客単価が高い


言語/フレームワーク

iOS:swift


Android:Kotlin


両用:React Native(Javascript
  • メリット:

  • デメリット:

    • まだ完全にワンソースで開発することはできない
    • カメラなどのos依存箇所の利用方法が弱い
    • 大規模なアップデートが必要になったりする
  • デメリットが多く、今選出する理由はなさそう


両用:Flutter(Dart


所感

  • 個人的にSwift、Kotlin、Flutterの3択

  • iOSを見据えるなら結局Macが必要で、MacBookProを買うかどうかが最大の悩みどころ

    • MacBookProを買うならSwift or Flutterから選ぶ

    • ひとまず買わないならKotlinを選ぶ

  • Macずっとほしいと思ってたから、これをきっかけに買ってしまうべきか悩む!


Swiftかflutterか





結論

  • あとは実際Mac買うかどうかなんだけど……

  • もう決めた!Swiftにしますか!

  • お金で解決できる悩みはお金で解決する主義です!

  • あと最後までFlutterと悩みましたが、最初つっかえるとまじで辛そうなので……

  • まずは作りやすさを優先したいと思います

    • OS依存機能をフルで使えるのはやはりでかいですね。

    • Androidに手を出す際に、KotlinではなくFlutterを考慮する方向かな!



おわりに

  • 今日はめちゃくちゃ悩みました

  • 要は17万強払ってiOS開発環境を手に入れる決断ですからね……

  • それ以外に関しては今のWindowsとGCEのUbuntu環境で満足してるのになー

  • ちょっと関わる人に軽く相談したら、Macポチろうと思います



f:id:kom314_prog:20210911145222p:plain
  • 買うなら13インチのM1スペースグレイSSD512GBかな!

  • 無駄にしないように頑張っていきたいと思います!

  • ではでは、ちゃお~~~!



小村のポートフォリオサイト開発(18) ページネーションを追加する(1)

  • こんばんは、小村だよ!

  • 今日も下記のポートフォリオサイトを構築していくよ

  • 前回storeを作成したので、今回はその機能を増やしていくよ!

  • よろしくね!!!



目次

  1. Entry取得時にページネーションを追加する
  2. ページ切替処理を追加する



記録

Entry取得時にページネーションを追加する

  • 現状はシンプルに、api/entriesにアクセスするとDBの全件を取得します

  • このままだと今後記事が増えた時に読み込みが大変ですね!

  • というわけでページネーションを追加していこうと思います。

  • 参考:シンプルブログ、記事一覧ページの作成③

    • この参考サイトもシンプルでいいですね
  • では早速実装していきましょう!


f:id:kom314_prog:20210906211151p:plain
  • ページネーション用のクラスを作成し、デフォルト値を設定します。

  • あとはviewpagination_class = EntryViewPaginationを加えれば完成

  • UIからAPIを呼び出すと下記のようになります


f:id:kom314_prog:20210906211415p:plain
  • responseにcount,next,previous,resultsが加わりました!

  • 返り値が変わったためデータを正しく取得できず、UIがエラーとなってますね

  • 正しく取得してあげましょう!


f:id:kom314_prog:20210906213400p:plain
  • データの格納場所がrequest.dataからrequest.data.resultsに移動しました。

  • これで取得するレコード数がPaginationで定義した数になりましたね。


f:id:kom314_prog:20210906213818p:plain
  • 3件だけだと上に寄ってる感がすごいな……



ページ切替処理を追加する

  • ではページを切り替えられるようにしていきましょう

  • まず、storeのentry.jsにpageを用意して、pageの値を取得、セットするgettars,mutation,actionsを用意します

  • それからfetchEntriesではurlを引数に取るようにしましょうか

f:id:kom314_prog:20210906223914p:plain


  • 次はコンポーネント側でstoreのpageを読み書きできるようにします

  • computedにpage:{get(){},set(){}}を記述することで実現できます

  • あとはこのpageの値に応じてURLを変更するgetFetchUrlを用意して

  • <v-pagenation>を操作時に再読み込みする処理を追加すれば完成!

f:id:kom314_prog:20210906224244p:plain


f:id:kom314_prog:20210906224323p:plain


f:id:kom314_prog:20210906224401p:plain
  • わーい!

  • ちゃんとページの切替できてますね!



おわりに

  • まだページ遷移処理は少し残ってます

  • 具体的には、ページネーション数をレコード数に応じて変化させたり

  • ソート順も降順にしたいですね。最新の記事が手前に欲しい

  • そのあたりを次回にやりたいとおもいます!

  • それでは!ちゃおちゃお~~~!



小村のポートフォリオサイト開発(17) API呼び出しをstoreに記述する

f:id:kom314_prog:20210904152907p:plain
  • サクサクっとサムネイル画像を表示できるようにUIを修正しました。

    • 修正箇所は割愛。
  • 画像入るとだいぶそれっぽく見えますねー

  • 今日は何をしようかなーーー

  • ひとまず今は単純にAPI呼び出ししてますが、今後にそなえてstoreに移動しましょうか。

  • そうしないと他の箇所で使うときに困ることになりますからねー



目次

  1. 現状整理
  2. storeを記述



記録

現状整理

f:id:kom314_prog:20210904154112p:plain



  • この場合、他のコンポーネントではAPIの値を取得できません

  • APIの呼び出しなどを1ヵ所で行い、このBrogCardはそこから値を呼び出すのが好ましいです

  • そこで出てくるのがstoreになります

  • 参考:Nuxt.js store

  • storeはvuexの仕組みを適用したNuxt.jsのフォルダになります

  • API以外にも、他のコンポーネントと値を受け渡したいときはこのstoreを経由します。



storeを記述

export const state = () => ({
  itemsloading: false,
  items: [],
})

export const getters = {
  getEntryItemsLoading(state) {
    return state.itemsloading
  },
  getEntryItems(state) {
    return state.items
  },
}

export const mutations = {
  SET_ITEMS_LOADING(state, bool) {
    state.itemsloading = bool
  },
  SET_ITEMS(state, items) {
    state.items = items
  },
}

export const actions = {
  async fetchEntries({ commit }, params) {
    commit('SET_ITEMS_LOADING', true)
    const response = await this.$axios.get('/api/entries/')
    const items = response.data
    commit('SET_ITEMS', items)
    commit('SET_ITEMS_LOADING', false)
  },
}


  • そしてBlogCard.vueを下記のように修正
f:id:kom314_prog:20210905183552p:plain
f:id:kom314_prog:20210905183617p:plain


  • このようにして、API呼び出し部分をstoreに記述し、それを呼び出すように変更できました

  • 画面表示は変わっておりません

f:id:kom314_prog:20210905183743p:plain
  • 注意点として、async awaitでデータを取得しに行っている間にその格納予定の変数を画面から覗くとエラー吐くので、ロード中はアクセスしないようにしましょう!

  • 上図の<div v-if="!getEntryItemsLoading">がその役割を担ってます



おわりに

  • 基礎と言いながらエラーと戦って数時間かかりました!

  • 業務ではコピペで済ましてた所だったので、改めて自分で調べながら書くと変なところでつまづいたりしますね。

  • 今後は、このstoreを拡張しつつ、ページネーションを作ったりカテゴリをちゃんと表示したりします!

  • ではでは、ちゃお~~~!



小村のポートフォリオサイト開発(16) データ加工 MD形式に図のURLを埋め込む

  • こんばんは、小村だよ!

  • 今日も下記のポートフォリオサイトを構築していくよ

  • 前回APIを利用してUI側にデータ取得するところまで完了しました!

  • 色々やることあるけど、今日はデータ加工を主にやっていきます!



目次

  1. HTML形式とMD形式の図の共通点を見つける
  2. 方針決め
  3. 実装



記録

HTML形式とMD形式の図の共通点を見つける

f:id:kom314_prog:20210904092123p:plain
  • 現状、取得したデータをそのままWebサイトに表示するのに問題があります。

  • まずWeb内で使用するデータは内容_MarkDownを使用する予定です。

  • しかし、MarkDown形式では図の通りはてなブログ前提のURL表記になってます。

  • これをちゃんとしたURLにしたものが、HTML形式のものに載ってますね。

  • というわけで、このURLを抜き出してMarkDown形式側を差替えする必要があります

  • まずは共通点を探すように、見比べてみましょうか

[f:id:kom314_prog:20210526210606p:plain]|
<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kom314_prog/20210526/20210526210606.png" alt="f:id:kom314_prog:20210526210606p:plain" title="" class="hatena-fotolife" itemprop="image">
  • ふむふむ!!!

  • 何個か比べてみてみましたが、ほとんど共通形式として下記が見つかりました


markdown

  • 頭は右記から始まる [f:id:kom314_prog:

  • 中間の数字はおそらくYYYYMMDDHHmmss

  • 中間の数字の最後のpは拡張子?(pはpng?)

  • 最後は:plain]|


html

  • 頭は右記から始まる <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kom314_prog/

  • 続きは、YYYYMMDD/YYYYMMDDHHmmss

  • 続きに拡張子 + "

  • 続きに alt="markdownの文字そのまま"



方針決め

  • これルールがわかれば、HTMLから取ってこなくてもMarkDownのを書き換えれますね

  • まずはMarkDownの中で、[f:id:kom314_prog:の文字を<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kom314_prog/に変換

  • [f:id:kom314_prog:の後ろの14文字の数字を取得し、左から8文字/14文字に変換

  • 後ろのp:plain].png">に変換

  • これでひとまず画像のURLを取得できそうですね。

  • ではコーディングしてみましょう!



実装

f:id:kom314_prog:20210904115642p:plain
  • 実装完了!

  • Markdown形式の内容の中の画像のURLが<img>タグに変換されてますね。

  • 最終的にはもっと別の形式に変えるかもしれませんが、ひとまずこれでOK

  • 最終的な修正箇所とソースを張り付けておきます



f:id:kom314_prog:20210904115930p:plain
f:id:kom314_prog:20210904120002p:plain
f:id:kom314_prog:20210904120048p:plain



import os
import requests
import xmltodict


class HatenaApi():
    HATENA_API_USER = os.environ.get('HATENA_API_USER')
    HATENA_API_BLOG = os.environ.get('HATENA_API_BLOG')
    HATENA_API_KEY = os.environ.get('HATENA_API_KEY')

    HATENA_API_URL_HEADER = 'https://blog.hatena.ne.jp'
    HATENA_API_URL_FUTTER = 'atom'

    IMG_SIMPLE_URL_HEADER = '[f:id:kom314_prog:'
    IMG_SIMPLE_URL_FOOTER = ':plain]'
    IMG_SIMPLE_URL_EXTENSION = {'p': 'png'}

    IMG_FULL_URL_HEADER = '<img src="'
    IMG_FULL_URL_TEMPLATE = 'https://cdn-ak.f.st-hatena.com/images/fotolife/k/kom314_prog' + \
        '/YYYYMMDD/YYYYMMDDHHMMSS.EXTENSION'
    IMG_FULL_URL_FOOTER = '">'

    def getAllEntries(self):

        all_entries = []
        url = self.getHatenaApiFirstUrl('entry')

        while url != '':
            hatenaApiData = self.getHatenaApi(url)

            for entry in self.getApiEntries(hatenaApiData):
                all_entries.append(self.formatEntry(entry))
            url = self.getHatenaApiNextUrl(hatenaApiData)

        return all_entries

    def getHatenaApi(self, url):
        auth = self.getHatenaApiAuth()
        hatena_list = requests.get(url, auth=auth)
        dict_data = xmltodict.parse(hatena_list.text, encoding='utf-8')
        return dict_data

    def getApiEntries(self, hatenaApiData):
        return self.getDictValue(hatenaApiData, ['feed', 'entry'])

    def formatEntry(self, entry):
        hatena_entry_id = self.getHatenaEntryId(entry)
        category = self.getCategory(entry)
        title = self.getDictValue(entry, ['title'])
        summary = self.getDictValue(entry, ['summary', '#text'])
        content_md = self.getDictValue(entry, ['content', '#text'])
        content_html = self.getDictValue(
            entry, ['hatena:formatted-content', '#text'])
        draft = self.getDictValue(entry, ['app:control', 'app:draft'])
        updated_at = self.getDictValue(entry, ['updated'], None)
        edited_at = self.getDictValue(entry, ['app:edited'], None)

        img_url_list = self.getImgUrlList(content_md)
        summry_img = self.getSummryImg(img_url_list)
        content = self.getContent(content_md, img_url_list)

        print(content)

        format_entry = {}
        format_entry['hatena_entry_id'] = hatena_entry_id
        format_entry['category'] = category
        format_entry['title'] = title
        format_entry['summary'] = summary
        format_entry['summry_img'] = summry_img
        format_entry['content'] = content
        format_entry['content_md'] = content_md
        format_entry['content_html'] = content_html
        format_entry['draft'] = draft
        format_entry['updated_at'] = updated_at
        format_entry['edited_at'] = edited_at

        return format_entry

    def getHatenaEntryId(self, entry):
        id_all = self.getDictValue(entry, ['id'])
        id = id_all[id_all.rfind('-') + 1:] if id_all else ''
        return id

    def getCategory(self, entry):
        category_all = self.getDictValue(entry, ['category'])
        if isinstance(category_all, list):
            category = category_all[0]
        else:
            category = category_all
        return self.getDictValue(category, ['@term'])

    def getImgUrlList(self, content_md):
        if not content_md:
            return ''

        # 画像の簡易URLを抜き出す
        img_url_list = []
        start_position = 0
        end_position = 0
        while content_md.find(
                self.IMG_SIMPLE_URL_HEADER,
                end_position) >= 0:
            start_position = content_md.find(
                self.IMG_SIMPLE_URL_HEADER, end_position)
            end_position = content_md.find(
                self.IMG_SIMPLE_URL_FOOTER, start_position) + len(self.IMG_SIMPLE_URL_FOOTER)
            simple_url = content_md[start_position:end_position]

            # 簡易URLから日付と拡張子を取得
            yyyymmddhhmmss = simple_url[len(self.IMG_SIMPLE_URL_HEADER):len(
                self.IMG_SIMPLE_URL_HEADER) + 14]
            simple_url_extension = simple_url[len(
                self.IMG_SIMPLE_URL_HEADER) + 14:len(self.IMG_SIMPLE_URL_HEADER) + 15]

            # 直リンク可能なURLを生成する
            full_url = self.IMG_FULL_URL_TEMPLATE
            full_url = full_url.replace('YYYYMMDDHHMMSS', yyyymmddhhmmss)
            full_url = full_url.replace('YYYYMMDD', yyyymmddhhmmss[:8])
            full_url = full_url.replace(
                'EXTENSION', self.IMG_SIMPLE_URL_EXTENSION[simple_url_extension])

            img_url_list.append([simple_url, full_url])
        return img_url_list

    def getSummryImg(self, img_url_list):
        if not isinstance(img_url_list, list) or len(img_url_list) == 0:
            return ''
        if not isinstance(img_url_list[0], list) or len(img_url_list[0]) == 0:
            return ''
        return img_url_list[0][1]

    def getContent(self, content_md, img_url_list):
        content = content_md
        for img_url in img_url_list:
            if not isinstance(img_url, list):
                break
            content = content.replace(img_url[0], self.getImgTag(img_url[1]))
        return content

    def getImgTag(self, img_url):
        return self.IMG_FULL_URL_HEADER + img_url + self.IMG_FULL_URL_FOOTER

    def getHatenaApiFirstUrl(self, action):

        url = [
            self.HATENA_API_URL_HEADER,
            self.HATENA_API_USER,
            self.HATENA_API_BLOG,
            self.HATENA_API_URL_FUTTER,
            action
        ]
        return os.path.join(*url)

    def getHatenaApiNextUrl(self, hatenaApiEntries):
        url = ''
        links = self.getDictValue(hatenaApiEntries, ['feed', 'link'])
        if isinstance(links, list):
            for link in links:
                if self.getDictValue(link, ['@rel']) == 'next':
                    url = self.getDictValue(link, ['@href'])
        return url

    def getHatenaApiAuth(self):

        return (self.HATENA_API_USER, self.HATENA_API_KEY)

    def getDictValue(self, dictData, keys, emptyValue=''):
        for key in keys:
            if isinstance(dictData, dict):
                if key in dictData:
                    dictData = dictData[key]
            else:
                return emptyValue
        return dictData



おわりに

  • せっかく今回URL変換したのに、詳細画面を実装してないから最終確認できない!

  • はやめに詳細画面実装したいなー

  • ではでは今日はこの辺で!ちゃお~~~!



小村のポートフォリオサイト開発(15) Axiosを使用したAPI呼び出し

  • こんばんは、小村だよ!

  • 今日も下記のポートフォリオサイトを構築していくよ

  • 今回は久しぶりにUI側を触るよ!

  • ここまで書いてきたAPIをついにUI側から呼び出しちゃおうと思います!



目次

  1. Axiosの利用
  2. CORSエラーを解消する
  3. トップページに表示



記録

Axiosの利用

  • ではでは、APIをUIから呼び出していきまっしょい!

  • 参考:Nuxt.jsでaxiosの使い方と設定方法を紹介

  • Nuxt.jsからAPIを呼ぶ際、Axiosというライブラリを使うことが多いです

  • Nuxt.jsでプロジェクト作成時に、私はAxiosにチェックを入れたのでばっちり導入済でした!

f:id:kom314_prog:20210901214521p:plain


  • じゃあさくっと動作確認用のコードをUI側に書いてさくっと実行しよう!

  • ほんとに動作確認のためだけのコードを書いたので実行!

f:id:kom314_prog:20210901221448p:plain
f:id:kom314_prog:20210901221518p:plain
f:id:kom314_prog:20210901221753p:plain



f:id:kom314_prog:20210901222025p:plain
  • エラーやんけ!!!!!!

  • びぇ~~~~ん!!!どらえも~~~ん!!!



CORSエラーを解消する

参考:【解決済】Docker上のNuxtでaxiosのCORSエラーにハマりまくった話

  • 解消方法は下記の2つがあるみたいです

    • メッセージの通りヘッダにAccess-Control-Allow-Originを付ける

    • プロキシを設定する

  • 今回は参考サイトの通りproxyを設定してみましょう。

  • nuxt.config.jsを開き、下記の通り変更します

f:id:kom314_prog:20210901224533p:plain
f:id:kom314_prog:20210901225746p:plain



f:id:kom314_prog:20210901225909p:plain
  • ひゃっほぉぉぉぉおおおおお!!!!

  • 無事取得できました!!!!!!!!!!



トップページに表示

  • せっかく取得できたのでサイトに表示してみます!

  • 色々エラーと格闘して最終的にこんな感じになりました!

f:id:kom314_prog:20210901234447p:plain
f:id:kom314_prog:20210901234523p:plain



おわりに

  • 今日はこれでおしまい!

  • やーーーっとスタートラインに立った感じですかね!

  • こうやって日を置いてみるとサイトが物足りないな……

  • 色々やること浮かんできますが、一つずつかたずけていきましょう!

  • ではでは、ちゃお~~~!