ã¯ããã«
å é²ãµã¼ãã¹éçºäºæ¥é¨ã®é«æ©ã§ãã主ã«ããã³ãã¨ã³ãéçºãæ å½ãã¦ãã¾ããä»åã¯ç§ãã¡ã®é¨ç½²ã§éå¶ãã¦ããã½ã¼ã·ã£ã«RSSãµã¼ãã¹ãNEIGHBORSããPWAåããéã«ãã£ããã¨ãæ¸ãã¦ãããã¨æãã¾ãã
NEIGHBORS | ã²ã¨ãã®èå³ãã¿ããªã®ç¥èã«
ã¨ã¯ãããæ¯è¼ççæéã§å®è£ ããã¨ããç®æ¨ãæ²ãã¦ããã¨ãããã¨ããããããã·ã¥éç¥ã¿ãããªããããã¤ãã£ãã£ã½ãæ©è½ã¯å®è£ ãã¦ãããããã£ãã·ã¥ã³ã³ããã¼ã«ããã¼ã ã«è¿½å ãã¦ã¢ããªã£ã½ãæ¯ãèãã¨ãã£ãæ¯è¼çç°¡æãªå½¢ã§è½ã¡çããã¦ãã¾ãããªã®ã§ããã®è¨äºã§ã¯PWAã®å®è£ èªä½ã«ã¤ãã¦è©³ããè¨è¼ãã¦ããã¨ããããã¯ãããã¾ã§åãã¦ããæ©è½ãPWAã¨ãããServiceWorkerã使ããã¨ã§ãã¾ãåããªããªã£ãç¹ã«éããç½®ãã¦ãã¾ãã
PWAã¨ã¯
PWA(Progressive Web App)ã¨ã¯ããã£ããããã¨ãWebã¢ããªã±ã¼ã·ã§ã³ã§ãããªãããã¤ãã£ãã¢ããªã±ã¼ã·ã§ã³ã®ãããªUXãå®ç¾ãããã®ã§ãã
PWAãå®ç¾ããããã«ã¯ä»¥ä¸ã®ä¸ç¹ãå¿ è¦ã«ãªãã¾ãã
- manifest.json
- Service Worker
- HTTPSã§ã®éä¿¡
manifest.json
manifest.jsonã«ã¯ã¢ããªã±ã¼ã·ã§ã³ããã¼ã ã«è¿½å ããéã®è¨å®ãè¨è¿°ãããã¨ãã§ãã¾ãã NEIGHBORSã§ã¯ä»¥ä¸ã®ããã«è¨å®ãã¦ãã¾ãã
{ "short_name": "NEIGHBORS", "name": "NEIGHBORS.", "icons": [{ "src": "touch/android-icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "touch/android-icon-256x256.png", "sizes": "256x256", "type": "image/png" }, { "src": "touch/android-icon-512x512.png", "sizes": "512x512", "type": "image/png" }], "start_url": "./?utm_source=pwa", "display": "standalone", "theme_color": "#e56c1b", "background_color": "#e56c1b", "description": "NEIGHBORS is an SNS sharing articles you read to your friends." }
short_name
ãã¼ã ã«è¿½å ããéã®å称
name
ã¢ããªã±ã¼ã·ã§ã³ã®å称ãAndroidã®å ´åã¯ã¹ãã©ãã·ã¥ç»é¢ã«è¡¨ç¤ºããã¾ãã
icons
icons
ã¯ãã¼ã 追å æã®ã¢ã¤ã³ã³ã¨ã¹ãã©ãã·ã¥ç»é¢ã§ä½¿ããã¾ãã
Chrome for Androidã§ã¯å
¬å¼ã¬ã¤ãã©ã¤ã³ã§ã¯192px X 192pxãæ¨å¥¨ãã¦ããããã®ãµã¤ãºãããã°æä½éåé¡ã¯ããã¾ãããï¼ä¸æ¦iPhoneã¯èããã«ï¼ãããã¤ã¹ã«ãã£ã¦ã¯ã¹ãã©ãã·ã¥æã«è¡¨ç¤ºãããéã«éåæãè¦ããã®ã§ã大ããç»é¢ç¨ã«ããï¼ãµã¤ãºç¨æãã¾ããã
start_url
ãã¼ã ããèµ·åããæã®URLãæå®ã§ãã¾ããNEIGHBORSã§ã¯GoogleAnalyticsãªã©ã§PWAããã®æµå
¥ãè¨æ¸¬ã§ããããã«./?utm_source=pwa
ã¨è¨å®ãã¦ãã¾ãã
display
display
ã¯ãã©ã¦ã¶ã®è¡¨ç¤ºã¢ã¼ãã«é¢ããé
ç®ã§ããstandalone
ãé¸æããã¨ç¬ç«ããã¢ããªã±ã¼ã·ã§ã³ã®ãããªå½¢ã«ãªãã¾ããä»ã«ãfullscreen
minimal-ui
browser
ã¨ããã¾3ãããstandalone
ãé¸ãã§ããã°å¤§ä¸å¤«ã§ãã
theme_color
ãã©ã¦ã¶ã®ãã¼ãã«ã©ã¼ãè¨å®ã§ãã¾ãããããAndroidã ãã§iOSã¯ããããè¨å®ã§ãã¾ããã
background_color
èµ·åæã®ã¹ãã©ãã·ã¥ç»é¢ã®èæ¯è²ãè¨å®ã§ãã¾ããiPhoneã§ã¯ãããã使ãã¾ããã
ãã®ä»ã«ãé ç®ãããã¾ããããã¡ãã«ã¤ãã¦è©³ããã¯ä»¥ä¸ãåç §ãã¦ãã ããã
ã¦ã§ãã¢ããªãããã§ã¹ã | MDN
iOSï¼Safariï¼ã§ã®å¯¾å¿
ä¸è¨ã§ããã¦è¨åããªãã£ãiOSï¼Safariï¼ã§ãããmanifest.jsonã§è¨å®ããå 容ãåæ ããã¦ãã¾ããã§ããã ã§ãã®ã§ã以ä¸ã®ããã«Metaã¿ã°ã§ãã©ã¦ã¶ã®è¨å®ãå¿ è¦ã§ãã
<link rel="apple-touch-icon" sizes="120x120" href="/touch/apple-icon-120x120.png" /> <link rel="apple-touch-icon" sizes="144x144" href="/touch/apple-icon-144x144.png" /> <link rel="apple-touch-icon" sizes="152x152" href="/touch/apple-icon-152x152.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/touch/apple-icon-180x180.png" /> <link rel="apple-touch-startup-image" href="/splash/ios-splash-640x1136.png" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" /> <link rel="apple-touch-startup-image" href="/splash/ios-splash-750x1334.png" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" /> <link rel="apple-touch-startup-image" href="/splash/ios-splash-1242x2208.png" media="(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" /> <link rel="apple-touch-startup-image" href="/splash/ios-splash-1125x2436.png" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)" /> <link rel="apple-touch-startup-image" href="/splash/ios-splash-828x1792.png" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" /> <meta name="apple-mobile-web-app-capable" content="yes" />
iOSçã®PWAã ã¨ã¹ãã©ãã·ã¥ç»é¢ã¯åã ã®ããã¤ã¹ã®ãµã¤ãºã«ãã£ãç»åãç¨æããå¿ è¦ãããå²ã¨æéããããã¾ããmanifest.jsonãã¨ã¯å¥ã«Metaã¿ã°ã®ç·¨éãããªããã°ãªããªãã®ãã¡ãã£ã¨é¢åã§ãã iOSãPWAã«å¯¾å¿ãã¦ããã¾ã æ¥ãæµ ãã®ã§ä»æ¹ãªãã§ãããä»å¾ã®ã¢ãããã¼ãã§ä»æ§ãçµ±ä¸ãã¦ãããããã¨ãæå¾ ãã¾ãããã
Service Worker
PWAãæ¯ããã®ã¯ãServiceWorker
ã¨ããä»çµã¿ã§ããã£ãã·ã¥ã³ã³ããã¼ã«ãããã¯ã°ã©ã¦ã³ãå¦çãªã©ãå®è¡ããã¹ã¯ãªããã§ãããã«ããPushéç¥ããªãã©ã¤ã³æã§ã®ç»é¢è¡¨ç¤ºãªã©ãå®è£
ã§ããããã«ãªãã¾ãã
create-react-app
ã使ã£ã¦ããã¸ã§ã¯ããä½æããã¨registerServiceWorker.js
ã¨ãããã¡ã¤ã«ãããã©ã«ãã§ç¨æããã¦ããããã«ããå®è¡ããã¨ServiceWorkerãä½æããã¾ããServiceWorkerãæå¹åããããã«ã¯ãã¢ããªã±ã¼ã·ã§ã³ããregister()
ãå¼ã³åºãå¿
è¦ãããã¾ãã
// src/index.js import { register } from './registerServiceWorker.js'; /* çç¥ */ register();
æãå ãã¦ããªãç¶æ
ã®registerServiceWorker.js
ã§ãéçãã¡ã¤ã«ã®ãã£ãã·ã¥ããã£ãã·ã¥ããªãã©ã¤ã³ã§ã表示ã«ããå¦çãå®è£
ãã¦ãããæä½éã®PWAã®æ©è½ãæºããã¦ããã¨è¨ãã¾ãã
ã¡ãªã¿ã«ãmanifest.jsonã®iconsãä¸ã¤ã§ãèªã¿è¾¼ããªãç¶æ ã«ãªã£ã¦ããããServiceWorkerã¯æå¹ã«ãªãã¾ããã
ã¾ãiOSï¼Safariï¼ã§ã¯ããã¯ã°ã©ã¦ã³ãå¦çãè¡ããã¨ãã§ãã¾ããï¼2019/03æç¹ï¼ãã§ãã®ã§Pushéç¥ãªã©ã®ãã¤ãã£ãã¢ããªã©ã¤ã¯ãªUXãç¾æç¹ã§ã¯å®ç¾ãããã¨ãã§ãã¾ããããã¡ããä»å¾ã®ã¢ãããã¼ãã«æå¾ ã§ãã
PWAåã§åããªããªã£ãæ©è½
PWAåããã¨æ®éã«åãã¦ããæ©è½ãåããªããªããã¨ãããã¾ããNEIGHBORSã§ã¯ä»¥ä¸ã®äºç¹ã§æå¾ ããåãããããå¦çã®æ¸ãæããå¿ è¦ã§ããã
- Facebookãã°ã¤ã³
- ã¢ã³ã«ã¼ãªã³ã¯ã§ããã¯ã¨ã³ãã«ãªã¯ã¨ã¹ããããå ´å
Facebookãã°ã¤ã³ãåããªã
NEIGHBORSã§ã¯ãFacebookãTwitterã®OAuthã§ã¢ã«ã¦ã³ããä½æããããã°ã¤ã³ã§ããããã«ãã¦ãã¾ãããPWAããã¼ã ã«ã¤ã³ã¹ãã¼ã«ããéã«ãFacebookã®OAuthã§ãã¾ãããããããªãå°ãã¾ããã
å ã ã¯ä»¥ä¸ã®ãããªæé ã§Facebookã®OAuthãå®è£ ãã¦ãã¾ããã
åå¾ããAPIãç¨ãã¦Graph APIã§ã¦ã¼ã¶ã¼æ å ±åå¾
1. ããã㧠Facebook SDK ãåæå
<script> window.fbAsyncInit = function() { FB.init({ appId : xxxxxxxxxxxxxxx, autoLogAppEvents : true, xfbml : true, version : 'v2.12' }); }; (function(d, s, id){ var js, fjs = d.getElementsByTagName(s)[0]; if (d.getElementById(id)) {return;} js = d.createElement(s); js.id = id; js.src = "https://connect.facebook.net/en_US/sdk.js"; fjs.parentNode.insertBefore(js, fjs); }(document, 'script', 'facebook-jssdk')); </script>
2. ç»é²ã»ãã°ã¤ã³ãã¿ã³ãä½æ
3. FB.login() ã§ã¢ã¯ã»ã¹ãã¼ã¯ã³ãåå¾ãå¿ è¦ãªãã¼ã¿ãåå¾
window.FB.login(response => { if (response.authResponse) { // ãã¼ã¯ã³ã使ç¨ãã¦ãååãåå¾ãã const fbApiUrl = `https://graph.facebook.com/me?fields=name&access_token=${response.authResponse.accessToken}` request.get(fbApiUrl).then(res => { if (res && res.ok) { // Do signin NEIGHBORS. } }).catch(err => { throw new Error('connection failed'); }); } }, { scope: 'public_profile,user_friends,email', return_scopes: true })
4. NEIGHBORSã«ãµã¤ã³ã¤ã³ / ãµã¤ã³ã¢ããããã
Facebookãã°ã¤ã³PWAç
ä¸è¨ã®æé ã ã¨ãèªè¨¼ç¨ã®ã¦ã¤ã³ãã¦ãå¥ã¦ã¤ã³ãã¦ã§éãã¾ãããèªè¨¼å®äºå¾ã«è¦ªã¦ã¤ã³ãã¦ã«ã¢ã¯ã»ã¹ãã¼ã¯ã³ãè¿ã£ã¦æ¥ãããã®å¾ã®å¦çãå®äºãã¾ããã
ãªã®ã§SDKã使ããã«å®è£ ãç´ãã¾ãããæµãã¨ãã¦ã¯ä»¥ä¸ã®éãã§ãã
1. èªè¨¼å¾ã«ãªãã¤ã¬ã¯ãããURLãFacebookãããããã¼ã§ç»é²ãã
Facebookãã°ã¤ã³ > è¨å® > æå¹ãªOAuthãªãã¤ã¬ã¯ãURI
2. react-routerã§ãªãã¤ã¬ã¯ãç¨ã®URLã追å
import React, { Component } from 'react' import { Provider } from 'react-redux' import { Switch, Route } from 'react-router-dom' import ReturnFromFB from './ReturnFromFB' ... class App extends Component { ... render() { return ( <Provider store={store}> <ConnectedRouter history={history}> <Switch> ... <Route exact path='/return_from_fb' component={ReturnFromFB} /> </Switch> </ConnectedRouter> </Provider> ) } } export default App
3. èªè¨¼å¾ã«ãªãã¤ã¬ã¯ãããå ã§ã®å¦çãå®è£ ãã
èªè¨¼å¾ã«ãªãã¤ã¬ã¯ãããã¨ãã¢ã¯ã»ã¹ãã¼ã¯ã³ããã©ã¡ã¼ã¿ã«ã¤ãã¦æ¸¡ãã¦ããã®ã§ã
window.opener.postMessage(message, targetOrigin)
ã§è¦ªã¦ã¤ã³ãã¦ã«ã¡ãã»ã¼ã¸ãéãã¾ãã以ä¸ã®ä¾ã§ã¯ãã®ãªãã¤ã¬ã¯ãURLãã¡ãã»ã¼ã¸ã¨ãã¦ãã¾ãã
import React, { Component } from 'react'; // PWAç¨ã®FBãã°ã¤ã³å¾ã®ãã¼ã¯ã³ãªã©ãã¢ããªæ¬ä½ã«æ¸¡ãããã®ã³ã³ãã¼ãã³ã class ReturnFromFB extends Component { componentDidMount() { // å¼ã³åºãå ã«å¯¾ãã¦ã¡ãã»ã¼ã¸ï¼ç¾å¨ã®URLã®æååï¼ããã¹ããã window.opener.postMessage(window.location.toString(), window.location.href); window.close(); } render() { return null; } } export default ReturnFromFB;
4. èªè¨¼ãã¤ã¢ãã°ãéã親ã¦ã¤ã³ãã¦å´ã®å¦ç½®ãå®è£ ãã
ã¾ãã¯ãã°ã¤ã³ãã¿ã³ã¯ãªãã¯ã§window.open()
ã§èªè¨¼ãã¤ã¢ãã°ãéãã¾ãã
ãã©ã¡ã¼ã¿ã¯client_id
redirect_uri
ãå¿
é ã§å¾ã¯ä»»æã§ãã
window.addEventListener('message', lisner)
ã§å°ã¦ã¤ã³ãã¦ããã®ã¡ãã»ã¼ã¸ãå¾
ã¡åãã¦ãlisnerã®ã³ã¼ã«ããã¯ã§URLã解æãã¦ã¢ã¯ã»ã¹ãã¼ã¯ã³ãåå¾ãã¾ãã
ãã®å¾ãGraph APIã«ã¢ã¯ã»ã¹ãã¼ã¯ã³ã渡ãã¦ã¦ã¼ã¶ã¼æ
å ±ãåå¾ãã¾ãã
async loginWithFacebookNoSDK() { const queryObj = { client_id: XXXXXXXXXXXXXXX, display: 'popup', scope: 'public_profile,user_friends,email', response_type: 'token,granted_scopes', auth_type: 'rerequest', redirect_uri: `${window.location.origin}/return_from_fb` } const url = `https://www.facebook.com/v2.12/dialog/oauth/?${this.buildQuery(queryObj)}`; const popup = window.open(url, 'Facebook Login'); const promise = new Promise((resolve) => { const listener = (event) => { const url = new URL(event.data); const params = url.hash.split('&'); let accessToken = ''; for (let i = 0, l = params.length; i < l; i++) { let s = params[i].split('='); if (s[0] === '#access_token') { accessToken = s[1]; break; } } const fbApiUrl = `https://graph.facebook.com/me?fields=name&access_token=${accessToken}` request.get(fbApiUrl).then(res => { if (res && res.ok) { // Do signin NEIGHBORS. } }).catch(err => { throw new Error('connection failed'); }); resolve(url); }; // ã¡ãã»ã¼ã¸ã¤ãã³ãããã³ããªã³ã°ããã window.addEventListener('message', listener); }); } // ãã©ã¡ã¼ã¿ãçµã¿ç«ã¦ã buildQuery(queryObj) { let queries = []; Object.keys(queryObj).forEach(function(key) { queries.push(`${encodeURIComponent(key)}=${encodeURIComponent(queryObj[key])}`); }); return queries.join('&'); }
5. ãµã¤ã³ã¤ã³ / ã¢ããå¦ç
以ä¸ã®ããã»ã¹ãçµã¦Androidã¯æå¾ éãã«åãã¾ããã
iOSã§ã®Facebookãã°ã¤ã³
iOSã®å ´åãä¸è¨ã«å ãããã«æãå ããªããã°ãªãã¾ãããã©ãããSafariã®å ´åã¯manifestã®è§£éã«ãã°ãããããã§ãã è¥å¹²å¯¾å¦çæ³ãªæãããã¾ãããpwacompatã¨ããã©ã¤ãã©ãªãèªã¿è¾¼ãã ä¸ã§ã以ä¸ã®ããã«ããã¯ããå¿ è¦ãããã¾ãã
<link rel="manifest" href="/manifest.json" /> <link rel="pwa-setup" href="/manifest.json" /> <script async src="https://cdn.jsdelivr.net/npm/[email protected]/pwacompat.min.js" integrity="sha384-GOaSLecPIMCJksN83HLuYf9FToOiQ2Df0+0ntv7ey8zjUHESXhthwvq9hXAZTifA" crossorigin="anonymous"></script> <script> let ios = !!navigator.platform && /iPhone/.test(navigator.platform); if (ios) { document.querySelector('link[rel="manifest"]').setAttribute("rel", "no-on-ios"); } </script>
ãªããAndroidã®å ´åã¯rel="pwa-setup"
ã ã¨PWAã®è¦ä»¶ã«æºæ ããªãã®ã§ãããã¨ã¯å¥ã«rel="manifest"
ã®ã¿ã°ãå¿
è¦ã«ãªãã¾ãã
åè
redirecting to Google OAuth flow in progressive web app
Remove the manifest from iOS to allow OAuth redirect to works
ã¢ã³ã«ã¼ãªã³ã¯ã§ããã¯ã¨ã³ãã«ãªã¯ã¨ã¹ããããå ´å
NEIGHBORSã¯react-router
ã¨ããããã±ã¼ã¸ã使ã£ã¦SPAãå®è£
ãã¦ãã¾ããreact-router
ã¯ããã³ãã¨ã³ãã§URLã®ã«ã¼ãã£ã³ã°ãè¨å®ã§ãããªãã¬ãã·ã¥ãªãã§ãã¼ã¸é·ç§»ã§ããä»çµã¿ãæä¾ãã¦ããã¾ããåºæ¬çã«ããã¯ã¨ã³ãã«ã¯Ajaxã§ãªã¯ã¨ã¹ããæãã¦ã帰ã£ã¦ããJSONãªãXMLãªã©ã®ãã¼ã¿ãå
ã«ã¬ã³ããªã³ã°ãè¡ãã¨ããå½¢ã«ãªãã¾ãã
ãã ãããã¯ã¨ã³ãã«ç´æ¥ãªã¯ã¨ã¹ããæãããå ´åãããã¾ããNEIGHBORSã®å ´åã¯èªãã è¨äºã«æ¢èªãã¤ããããã«ãä¸æ¦ããã¯ã¨ã³ãã®å¦çãçµç±ããã®ã¡ã«ã¯ãªãã¯ããè¨äºã«ãªãã¤ã¬ã¯ãããããã¨ããä»çµã¿ãåå¨ãã¦ãã¾ããã ãªã®ã§ãã®å ´åã¯ãå¥ã¿ãã§ã¢ã³ã«ã¼ãªã³ã¯ãéãã¦ããã¯ã¨ã³ãã«å¦çããä»»ãããã¨ããå½¢ã«ãã¦ãã¾ããã
ããã§åé¡ãçºçããã®ã§ãããServiceWorkerãæå¹ã«ãªã£ã¦ããã¨åããªãªã¸ã³ã§ãã¤ServiceWorkerã®ã¹ã³ã¼ãã®ç¯å²å ã®URLã®å ´åããµã¼ãã¹ã¯ã¼ã«ã¼ ãããªã¯ã¨ã¹ããè¿ãã¨ããä»çµã¿ãããã¾ãã NEIGHBORSã§ã¯ãã¡ã¤ã³é ä¸ã¯å ¨ã¦ãã¹ã³ã¼ãã¨ãã¦ããã®ã§ãããã¯ã¨ã³ãã¸ã®ãªã¯ã¨ã¹ãã渡ãããstatus 200 ok ã§ããç»é¢çã£ç½ãã¿ãããªç¶æ ã«ãªãã¾ããã
èãããã解決ã¯ä»¥ä¸ã®ä¸ç¹ãã¨æãã¾ãã
- ServiceWorkerã®ã¹ã³ã¼ãã®ç¯å²ãå¤æ´ãã
- æ¢èªå¦çã®URLã®ãã¡ã¤ã³ãå¤æ´ãã
- å®è£ æ¹æ³ãè¦ç´ã
ï¼ã«ã¤ãã¦ã¯ã¹ã³ã¼ãã¯ä¸é¨URLãé¤å¤ãããã¨ãã§ããªãã®ã§ãå ¨ã¦ã®URLãå¤ããå¿ è¦ãããã¡ãã£ã¨ããã¯åé¡ãããã¾ããã
ï¼ãæ£æ»æ³ãªã®ããããã¾ãããããããããã¨ã«å¯¾ãã¦å¤å°å¤§ããããªã¨æã£ãã®ã§ãä»åã¯ãã¹ãã¾ããã
ã¨ãããã¨ã§ãï¼ãæ¡ç¨ãã¾ããã ä½ãã©ãå¤æ´ãããã¨ããã¨
- æ¢èªå¦çã¯æ¢èªå¦çã ããããããã«å¤æ´
- Ajaxã§æ¢èªå¦çã®APIãå©ã
- ã¹ãã¼ã¿ã¹ã³ã¼ãã204ã®å ´åãæ¢èªã¢ã¤ã³ã³ãã¤ãã
- æ¢èªå¦çãæåã®å ´åãå¥ã¿ãã§è¨äºãã¼ã¸ãéã
ã¨ããæãã§ãã
æå¾ã«
éåã¨ã ãã ãã¨æ¸ãã¦ãã¾ãã¾ããããããã·ã¥éç¥ãªã©ãããã¤ãã£ãã©ã¤ã¯ãªæ©è½ãå®è£ ããªããã°ãããã»ã©ãã¼ãã«ã®é«ããã®ã§ã¯ãªãã¨æãã¾ãããç¹ã«SPAãµã¤ãã ã¨ãã£ãã·ã¥ã¨ãªãã©ã¤ã³å¯¾å¿ãã§ãã¦ããã°ããã ãã§ãååã«ãã¤ãã£ãã£ã½ãæãã«ã¯ãªãã®ã§ã ä»å¾ãiOSã®å¯¾å¿ãé²ãã§ãããã¨æãã¾ãã®ã§ãå åããã¦å¯¾å¿ãã¦ããã®ãè¯ããã¨æãã¾ãã
ãã¨ãNEIGHBORSã®Twitterãã£ã¦ã¾ãããããã°ãã©ãã¼ãé¡ããã¾ãm( )m