SwiftUI Map 获取当前地图位置

 • 

SwiftUI 在 WWDC 2020 新加的 Map 组件虽然没有 UIViewRepresentable 自定义包的功能丰富,但也可以做一些复杂的需求了,比如获取当前位置。
Map 的参数里 coordinateRegion 是一个 MKCoordinateRegion 的 Binding,地图移动的时候会反馈到 Binding 里面,不用再去想办法做类似于 WKMapView.Appearance.delegate 之类的全局 hack 了,只用声明一个 ObservableObject 然后在里面写 didSet:

final class MapConfig: ObservableObject
{
    var region: MKCoordinateRegion = MKCoordinateRegion(...)
    {
        didSet {
            print("\(region)")
        }
    }
}

有 didSet 后就可以做一些跟随功能,比如放一个 Pin 一直跟随用户拖动:

import SwiftUI
import MapKit

struct MapAnnotationModel: Identifiable {
    var coordinate: CLLocationCoordinate2D
    let id = UUID()
}

final class MapConfig: ObservableObject
{
    var region: MKCoordinateRegion = MKCoordinateRegion(center: CLLocationCoordinate2DMake(37.8, -122.5), latitudinalMeters: 5000, longitudinalMeters: 5000)
    {
        didSet {
            annotationItems = [MapAnnotationModel(coordinate: region.center)] // 替换 annotationItems 为新的带有 region center 的数组
            self.objectWillChange.send() // 提醒 UI 更新
        }
    }
    
    @Published var annotationItems: [MapAnnotationModel] = [] // 默认空
}

struct ChooseLocationView: View {
    @ObservedObject var config = MapConfig()
    
    var body: some View {
        Map(coordinateRegion: $config.region,
            interactionModes: .all,
            showsUserLocation: true,
            userTrackingMode: nil,
            annotationItems: config.annotationItems)
            { item in
                MapPin(coordinate: item.coordinate)
            }
        .edgesIgnoringSafeArea(.all)
        .navigationTitle("Choose Location")
    }
}

2020.11.30

 • 

有一个月没写博客了,不知道何时视博客为周记的我也开始懈怠起来。补一下这一个月做了什么。

月初拿到了 CMU INI 学院 MSMITE 项目的录取,一个本科逃学的学渣能拿到这个神仙学校和项目我已经没有什么怨言可以说了,因此把 CMU ETIM 的项目给退了,花费了点时间,然后就在准备选课,i20 和健康证明,估计还要一两周才能办好。最近在看选课,说实话对于能不能应付课程还是有点虚。

月初在青岛住了一两周,很开心的日子,和好朋友们到处玩,买好吃的面包,在岛上闲逛,天气不好的时候就在屋子里写 Age Clock,后来 Age Clock 的 Catalyst 版本也被 Mac App Store 的 Big Sur Widget 选集精选了,不是什么很花时间的软件,但还是很开心。

办了柬埔寨和新加坡的签证,完全是为了能作为第三方国家中转去美国而申请的,现在也都批了下来,只等学校的手续走完了。

买了 M1 芯片的 MacBook Air,日常用的确还不错,不足的地方就是非定制款的 8G 内存太少,以及软件适配还不是很全,Unity 和 Xcode 目前都可以,但 JetBrains 全套(Rider,PyCharm,WebStorm)包括 Android Studio 都挺卡的。这款 MacBook 让我想起了 14 年上大学之前拿到的第一款 13 寸MacBook Pro,轻便的机身,不错的性能,续航长也不发烫,适合带着到处耍。

买 MBA 的钱基本上就来自于在成都取的公积金,反正也没有在成都买房的打算,原本的公司也只是注册在那里而已办公还是在北京。关于公司的事情我现在基本上已经看开,原本的创始团队据我所知也变动了不少,反正就还是不一条道的人,做事方法不一样而已,没必要让自己那么不开心。

下旬在写 UniLWP.Droid 的第二版本和文档,最近提交了审核。这版本修了一些缺失 context 导致的 Unity 崩溃问题,以及一些新的拓展特性。Unity Asset Store 在 11 月税后挣了 200 刀左右,考虑到单价 60 刀的 UniLWP 我几乎没在 Unity 社群里做过宣发,基本上是一个比较满意的成绩了。

还有很多事情要做,比如我还要不要投递 Summer Intern,我的 Side Project 还有没有时间,以及我的 i20 什么时候能发下来,我的 F1 签证什么时候可以预约,后面的租房和 Career Fair。也不能说自己很慌,毕竟我也没到那种精神状态,但忧虑还是有的。

更新了下 个人主页 适配了 dark mode。可能 Portfolio 也要更下,但 vue 3 还没稳定,前端一堆 precompiled lib 在 arm 上适配也没做完的样子。

听了 Under the Hood

2020.10.17

 • 

打开 Ghost 看了半天不知道要写什么,刚有了些想法弹出了一封邮件,打开一看是说我五月份答应的 bugfix 还没推到商店里,用户表示非常失望云云。邮件看多了我也佛了,也没有回信的冲动,只是想到的想法又没了,因此就记点流水账吧。


前几天卡耐基梅隆的创新管理项目发了录取通知,这个项目原本是和计算机合并申请的类似于双学位的项目,之前的博客里还提到在青岛玩的时候被创新管理学院远程面试到绝望的事情,当时的面试官最后说他们会等计算机学院出录取意见后给出答复,而最后的答复就是计算机学院嫌我菜,因此双学位变单学位,成了一个一年的项目。但好处是可以当年暑假就实习,OPT 之类也有 extension,我也没有抱怨的资本,能去 CMU 读一个类似于 MBA 的 ETIM 对我已经是很幸运的事情了,而且找工作也没人关心我具体什么专业的吧。
因此就又开始考虑找暑假实习的事情,但我一点头绪也没,想安心刷题这几天也不太可能,爸妈想让我近期陪他们旅游。我自己也觉得就要出国了,多陪下家人好像也是应该的,只是很多时候感觉家人把我的事情要么想的太难要么想的太容易,而我为之铺垫各种的小心谨慎,似乎他们都刻意地忽视了。有段时间他们觉得我过去找不到工作,现在又兴致勃勃地拽着我出去玩好像我什么都不用干就能直接过面试一样。去柬埔寨面签的事情也是一样,每次我和他们说他们都会把话题转移到那边安不安全的问题上,而没有去和我一起研究我要是去的话需要准备的东西,我说我不关心那边安不安全只关心这条路能过去的话他们又觉得我不把自己当回事,开始说落一番,最后真正需要解决的事情,不知道他们是明白但潜意识刻意回避,还是真的因为了解不多,反正是没有解决的尝试的,连带着我也或多或少地跟着多了些躲避的态度在里面。
我其实很痛恨这种躲避的状态,而且我自己知道如果跟着一起这样一点主见也没,可能我最后连 21 Spring 都没办法按时赶过去。但我只是想和父母保持信息上的同步而已,就像是大学翘课的时候出于为了保持家长对我的支持而不让他们偏向辅导员的理由而和父母经常打电话一样,我只是想让他们明白我到底在干什么,倒不一定要旗帜鲜明地支持我,但至少不至于因为信息上的差异让他们觉得我在干傻事。大学翘课的事情,虽然在今年的留学申请里让我吃了不少苦头,但至少让我在和父母的协商里有了点资本,外加上一直保持着沟通,最后父母的意见还是慢慢偏向了我这边,能让我在没有他人干预我人生规划前提下完成了从靠父母到经济独立的过渡,也没出现因为我要退学父母威胁断绝生活费之类的事情,所以我觉得能让父母和自己保持在同一个阵线还是挺重要的,但留学的事情似乎已经到了某种认知的边界,我也越发觉得可能真的会有一些我再怎么铺垫最后都无济于事的情况,只能该办的都先自己办了,临到头再解释,就像是今天他们才知道为了保持工作身份要有很长时间没办法回国一样。


最近看了两集 Netflix 上的挑战者号纪录片,再加上之前看协和客机退出民航业务的纪录片,感觉人类好像从两千年开始停止了发展一样。这种感觉在看纪录片里面新闻播报员兴奋地说太空飞行在十年内会走向平民大众的时候最甚,好像是完全不同的一个文明。


去上海跑了一圈,见了要去北欧读书的大学同学。可能是最佛系的一次约饭了,从年初到人都要出国了才约到,见了面两个人也是各种佛,吃什么都行,餐厅排号叫号过了也无所谓,大不了再排一次。不过一直操心出国留学这些虚无缥缈的未来规划,也很难不佛起来。他就要去展开新生活了,我因为春季才开学,估计还要再佛上一段时间,直到下次动身。


听了 Selected Sessions MK b2b Sonny Fodera U-Bahn DJ Set,看了 DEFCON scares me and science says it will scare you too

用 Slice 来做 Android Live Wallpaper 的快捷设置项

 • 

好久没写技术实现了,水一篇好了。
Slice 是 Google 做的一套模版化 UI,主要用于 Assistant 相关的界面展示,但系统设置顶部偶尔也会出现相关的应用。B-Reel 和 Google 在 Pixel 4 自带的 Pixel Live Wallpaper 里用 Slice 来做了一组快捷设定项,效果如下:
PixelLWP
作为专业 Pixel Live Wallpaper 高仿者(误,做新软件 Diorama 的时候我也试了一把 Slice,效果还是可以的:
Diroama
至于原理其实很简单,在 API 29 上,wallpaper.xml 可以添加一个新字段 android:settingsSliceUri,如果提供的 Uri 是正确的,动态壁纸 Preview 的底部 panel 会新增一个 customize 的 tab。Diorama 目前的 wallpaper.xml 如下:

<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsSliceUri="content://com.justzht.lwp.diorama/settings"
    android:showMetadataInPreview="true"
    android:description="@string/app_wallpaper_description"
    android:author="@string/app_author"
    android:contextUri="@string/app_wallpaper_context_uri"
    android:contextDescription="@string/app_wallpaper_context_description"
    android:label="@string/app_name"
    android:settingsActivity="com.justzht.lwp.diorama.view.activity.MainActivity"
    android:thumbnail="@mipmap/wallpaper_preview"/>

Slice 的实现也很简单,首先 Android Studio 的新建菜单里 New/Other/Slice Provider 这个路径下就已经提供了一个基础的模板,只需要自己拓展 public Slice onBindSlice(Uri sliceUri) 方法返回一个自定义的 Slice 即可。

ListBuilder builder = new ListBuilder(context, sliceUri, ListBuilder.INFINITY);
if ("/settings".equals(sliceUri.getPath())) {
    // add your own implementation here
    return builder.build();
} else {
    return builder.addRow(
        new RowBuilder()
        .setTitle("ERROR: URI " + sliceUri.getPath() + " not found.")
    ).build();
}

Slice 的 Template 里有一些可以交互的 UI 元素,比如 Toggle 或者 Slider。这些交互的触发事件可以通过设定各自元素 Builder 里的 Action 来实现,比如 ListBuilder.InputRangeBuilder 就可以调用 setInputAction(PendingIntent action) 来触发滑动事件,以及 setPrimaryAction(SliceAction action) 来触发整个 row 的点击事件。若是要做一个时间拖动的 Slider:

// init your ListBuilder before this code snippet
int hour = (int) Math.floor(24 * Utils.getVal(SettingsManager.getInstance().getModel().dateTimeManualModeProgress, 0 f)); // Get current set hour from some where
Intent intent = new Intent(context, SliceBroadcastReceiver.class).setAction(context.getString(R.string.intent_change_datetime));
ListBuilder.InputRangeBuilder inputRangeBuilder = new ListBuilder.InputRangeBuilder()
    .setTitle(context.getString(R.string.text_time))
    .setSubtitle("Custom at " + String.format("%02d:00", hour))
    .setInputAction(PendingIntent.getBroadcast(
        getContext(),
        0,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT
    )) // add a PendingIntent so a BroadcastReceiver can receive the event
    .setValue(hour) // note that value is an int, meaning this slider is a *stepped* seekbar
    .setMax(24)
    .setMin(0);
builder.addInputRange(inputRangeBuilder);
// return builder.build() after this code snippet

然后在 BroadcastReceiver 里就可以获得 Slider 更新的值:

public class SliceBroadcastReceiver extends BroadcastReceiver {
    private static Uri sliceUri = Uri.parse("content://com.justzht.lwp.diorama/settings");
    @Override
    public void onReceive(Context context, Intent intent) {
        String intentAction = intent.getAction();
        if (intentAction != null) {
            if (intentAction.equals(context.getString(R.string.intent_change_datetime)) &&
                intent.getExtras() != null &&
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                int hour = intent.getExtras().getInt(EXTRA_RANGE_VALUE, 0); // acquire the updated value using EXTRA_RANGE_VALUE
                Utils.setValIfNotSame(SettingsManager.getInstance().getModel().dateTimeManualModeProgress, hour / 24 f, false); // save this value to somewhere

                context.getContentResolver().notifyChange(sliceUri, null); // notify the app to refresh slice
            }
        }
    }
}

最后记得调用 void notifyChange(Uri uri, ContentObserver observer) 刷新 Slice 即可。

2020.10.1

 • 

手头两个项目都还没做完,自己又多了几个新东西的想法,只感觉自己步调越来越不一致了。
我对项目,或者说,产品定位,一直没什么特别的想法。很多东西其实只是觉得好玩或者自己用,然而在做的过程就会考虑独立开发的盈利问题,就比如说如果不是为了可能的盈利,我其实是很不想在 Google Play 上做免费+内购软件的,原因倒也不是内购造成的用户差评或是免费造成的服务器负荷,而就只是我不想专门把时间拿来写 IAP 支付的逻辑上,对于我来说这块代码完全是没有乐趣可言的。然而总没有那么完美的事情,只能在让自己爽和让用户满意之间踮起脚跳来跳去。
昨天下午起床后就一直在写 Diorama 的 Firebase Auth + Database 逻辑,我觉得可能之前几个地理位置的软件没有增长的原因是缺少用户 UGC,毕竟里面内置的 Presents 或 Collection 都是我自己维护的,而我又是一个极度喜新厌旧的人(哪个 Android 用户又不是呢),因此自己的项目过半年就会直接放弃维护转而去研究新玩意,这一来用户也自然没有增长。但要做 UGC 成社区吧,我又觉得一个简单的小软件没必要大张旗鼓,社区规则弄不好就可能全是 SPAM 和 Bot,数据库每个月扣我的钱都会比卖软件的收入多。
写到晚上还有好多逻辑没完成,再一看调试就已经用掉了单日五万次 API 里面的两千次,只觉得做 UGC 好难啊,我还是挺发怵的,就还是先把 Diorama 做成一个没太多用户间互动的纯单机软件吧,把自己更感兴趣的动态天气和交通模拟功能给写了就准备上架了,我做 Diorama 的起源也只是觉得能随时看到一个微观世界挺开心的,Diorama 在特定时间展示出来的效果也的确挺好看,虽然我还不确定用户会不会喜欢罢了。

Banner

2020.9.23

 • 

前几天去青岛玩了,十月份会再过去常住一段时间。
独立开发者群里貌似因为 iOS 14 的缘由好多 Widgets 软件都被首页推荐了。看大家很开心的样子,我也开始想着要去更新下自己的 GitHub Contributions 了,不过貌似 WidgetsKit 还不能直接套 SceneKit,只能主 app 渲染成图再传回去,还需要时间去研究下。此外最近在研究 Metal,可能回头会做 Diffuse for Mac。
Google Play 的收入日渐下降,为了保证收入又在做几个新 LWP 了。Unity Asset Store 的 UniLWP 这个月也卖出去几份,虽然需要负责技术支持有点花时间。
感觉坑开的有点多,不过是件好事,至少有想法了。感觉多多跑动对自己也挺好的,因此还在计划月底再去北京一趟。
在路上听了 Session 042Groove Monday 08.31.20Find My Way Home。以及想起来在青岛的第一天早上收到邮件,是加申的硕士项目在约时间面试。接着晚上刚吃完饭就有视频邀请,问的问题也极尽尖锐,我于是花了一番功夫去解释为什么本科 GPA 那么差,可能说到最后我自己都有点难堪。挂掉 ZOOM 后躺在酒店的床上反而突然很放松,毕竟前段时间一直在准备 Coding 面一直没面成,反而阴差阳错在旅途中被问了那么多 Behavior Question,我也脑子缺根筋地说了很多看法和坏话,可能是我近期最坦诚相见的半小时了。

2020.9.15

 • 

Google 的招聘网站不知道出了什么问题,我点开之前投递的 21 Summer Intern 简历申请只会出现无法加载的提示,不知道是不是因为写了国内的地址所以被系统自动过滤掉了。摊手。
上周拿到了 Persona 5 Scramble 的卡带,因此花了两天打通关。P5S 的剧情算是接着 P5 来的,还是高中生的奇幻冒险故事。打完后产生了奇妙的想法,可谓逃避现实,也可以称之为想开了,即:我已经不像本科那时一样有追求真正幸福的勇气了,这两年反而在为一些现世的东西而牵挂,比如工作的薪资,简历上的头衔,还有一些他人的所谓成就,譬如谁谁拿了 Forbes 30,谁谁融了多少钱,包括前段时间一直在纠结 defer 的这段时间是在国内实习还是做独立游戏也是这类问题,抛开自己不知何时产生的世俗眼光,我当然更愿意去做完一直没做完的太空游戏,因为那才是能让我感到开心的事情,而最多只有三个月的实习,更多的是出于对自己长时间没在公司工作的焦虑而产生的想法,是我感到有义务要去做但并不真正喜欢的事情。然而回过头来看,这些我强加到自己身上的义务,更多的是大家认为的成功人士该做的事情罢了,我似乎也没有义务去为此而焦虑。
于是打通关 P5S 后,我就没在考虑找公司的事情,而是继续做独立开发了。一款游戏的剧情能让我自省,给我这么多信心,实在是近期买的最值的游戏了,特别是考虑到这剧情若是突兀地解释给他人,可能我半天也只能说出“高中生拯救世界”这种中二的剧情,和内省这种严肃的行为毫不搭边,但游戏给人的震动却是真实的。

2020091013031400-19E0B7B692434F091AC0D5181A6ED69D

2020.9.6

 • 

在家看了一会算法,原本是给 Google 的 Summer Intern 准备来着,但看目前这招聘形式估计希望不大,因此目标主要转变为月中试下国内几家的面试。虽然过了也不一定去,但好久没接触过公司了,面一面至少能让自己有点实感。
此外,最近基本就是看别人做什么自己也就做什么,看延期入学的人里面有人在国内先实习了,自己就想着要不要也找家,但最多就干三个月 HR 好不好答应不说,自己手上还有几个个人项目,也并不是很有要工作的紧迫性和觉悟。然后看到一直在追的某些开发组甚至个人工作室开发的游戏已经出 trailer 上 IGN 的 YouTube 频道了,又痛心疾首没有把时间花去做 Epoch 上。
听了 SoundOf: Sam Lowe X GuyMac,这首最近变成了跑步专用歌。还听了 So Far AwayIt's Probably MeSweet and Lovely。看了 TENET,买了票明天二刷。某个犯困的中午还看了半个小时的听见涛声,要记得这周找个时间看完。
好想去日本玩啊。

2020.8.20

 • 

东北 OOD 这门网课终于结了,大作业熬了一整夜其实还是没做完,只不过演示的效果还行。之后还是准备延期,毕竟在家上研究生对于我来说实在是太憋屈了,不如不上。
推掉了一些事情,准备安心刷题了。
听了 Never let you goLess Is More