从小猫补光灯看独立开发:十五分钟与拒绝自嗨

安迪·沃霍尔

每个人都能成名十五分钟

小猫补光灯的爆火,已经在我所在的开发者群里掀起了长久的讨论。我也有很多自己的看法,但碍于我最近在赶新App的内测版本,所以今天内测发版之后,就来写一写。

对于不了解小猫补光灯的读者,这里是原作者对整个App开发和运营的复盘,可以先补充一下背景知识:
https://mp.weixin.qq.com/s/OjrbaIcMvJJNY-IA1G6cDQ

那么开始进入正题。首先来说,作为一个马上要迈入10年大关但完全没有作品火过的iOS独立开发者,说不酸那是不可能的,但酸过之后,我也愿意承认,这件事给我带来的启发是要大于嫉妒的。

今年以来,我不止一次见识到了流量对于独立开发的影响。从最开始的胃之书,到勇敢牛牛和牛牛挤奶,再到现在的小猫补光灯。这三个作品不止有流量很高一个共同点,另一个更重要的共同点是,他们都准确的发掘了用户的需求。这一点在除了胃之书外的后两者身上更加明显。如果说胃之书还是依靠着AI进步来实现的对旧有需求的升级改造这样的常规进化,那么牛牛和补光灯几乎就会让很多颇有经验的独立开发者感到惊讶,我想他们会和我是一样的第一反应:啊,这个需求也能/用做么?

然而这其实就是很多现在的独立开发者的局限性了。这份局限性有自身的经验,也有苹果审核驯化影响的结果。我上面提到这两个App时,分别用了“能”和“用”这两个字。对于牛牛来说,大家的第一反应应该是这不违规么?对于补光灯来说,则会想这个也太简单了,不会被4.2/4.3么?(简单解释下苹果审核的4.2和4.3条,分别是应用过于简单和同类应用过多)然而实际的结果就是这两个App不仅发了出来,而且因为精准拿捏了用户需求,叠加上流量的影响,直接一飞冲天了。

雷军说过:风口之上,猪也能飞起来。

但这里隐藏了两个重要的节点,一是找准风口,二是敢往下跳。在补光灯的作者的复盘中也提到了,很多人会认为他买量,或者靠资源置换才让补光灯火起来以及冲榜的。但我会更倾向这一切都是流量加成下的马太效应,一个很简单的道理,如果你是小红书/B站的科技板块编辑,有一个现成的火爆话题,为什么不用呢?当然了中肯的说,如果要我说谁在补光灯的爆火中居功至伟,我会把这个奖项颁给作者的女朋友,如果没有她,陈云飞只会是一个用AI做了个屏幕手电筒的App的AI布道者,而不会是现在这个做出了小猫补光灯的AI独立开发者了。

所以以上的所有论述其实就可以总结为一句话:找准需求,对于独立开发者来说,才是最关键的。

我见过有太多的独立开发者(包括我自己),会为了一个需求而感到自嗨,上线之前觉得肯定要火,上线之后发现下载个位数。而在独立开发者群里在聊为什么设计、产品、运营在掌握了AI编程能力之后都能对程序员进行降维打击时,大家可能会觉得设计能设计出好看的界面(美也是一种需求),运营可以找到流量,但产品不也就和程序员一样靠想法取胜么?这时候一位大佬的发言就很一针见血:产品在选品环节没那么自嗨,可以少走弯路。

当然也不是说就要一棒子敲死所有的程序员出身的独立开发者,程序员出身又大放光彩的大有人在。但对于更多仍挣扎于茫茫App海洋中艰难游泳的独立开发来说,希望我们每遇到一个新的需求,多想个一两层,稍微做做调研。不要觉得搜不到竞品就是发现了新大陆,AppStore中已经有了几百万个App,和你有同样想法的人,很难不会找出另一个。只有找准了需求,才有可能遇到属于自己的“成名十五分钟”。

最后,请允许我以一句稍显悲观的话做出总结,但这句话也可能会让陈云飞不至于对我把最佳贡献奖颁给他的女友这个行为那么愤怒:

每个人都可能成名十五分钟,但有的人,也许天生的就是半个小时,一个小时,甚至更长。

如何绕过ChatGPT针对服务器提供商的IP封禁

今天早上一起来,发现新App审核又被拒了,但理由又是他们找不到IAP项目,遂怀疑是他们对汉语引导的理解问题,写了一段非常详尽的引导,然后打开ChatGPT,想翻译成英文,结果发现ChatGPT把我的IP ban了。

一开始,还以为也是其他人遇到的账号被封问题,但仔细一看是禁止了我的IP访问。我一直是用自建在Vultr上的服务器来进行专业上网的,虽然价格可能比某些服务贵些,但贵在稳定和安心。(5刀一个月,1000G流量,其实也还能接受了)

于是我在Vultr上直接新建了一个服务器,启动好之后配置一下,发现依然不行,不论是ipv6还是ipv4,也都不行,于是我想到估计是Vultr整体被封了,去搜了一下,才发现原来我还算是幸运的了,早两个月开始ChatGPT就已经开始大规模封禁来自各个云服务提供商的请求了,aws,GCP,Vultr这些大的提供商更是早就上了黑名单。但搜索之下,也找到了应对的办法,那就是利用魔法打败魔法。

ChatGPT是利用cloudflare来进行网络防护的,而cloudflare家自己,却出了一个安全上网和反嗅探的工具:Cloudflare Warp,也就是著名的1.1.1.1。

利用这个工具,我们只要在服务器端配置好了,既可以实现穿越ChatGPT的封锁。(注意,是在服务器端安装,在你本机安装并没有用,所以如果你并不是自有服务器,那么后续的内容对你来说可能用处就不大了)

下面是具体的操作步骤:

首先,登录你的服务器(比如ssh或者在云服务的官网找对应工具),在命令行里输入:

如果你的服务器是基于apt的(例如Ubuntu或者Debian)
sudo apt install cloudflare-warp
如果你服务器是基于yum的(例如centOS或者RHEL)
sudo yum install cloudflare-warp
安装好之后,继续运行如下的代码:
warp-cli register
如果成功,会显示一个Success

接下来这步比较关键,而且其实是有限制的一步,因为一旦开启warp之后,我们本地对服务器的访问,其实也会被限制,我目前只找到了一个添加例外IP的做法(其实还有个同样是cloudflare提供的zero trust的将你的服务器和本机组成类似内网的解决方案,但这个方案我个人是感觉限制过多而且过于依赖cloudflare了,万一哪天它也反了,就。。)

但添加例外IP,其实就是将本机的IP地址添加到warp的例外中,而我们都知道,除非你自己拉了根企业光纤有独立的IP地址,否则家用宽带的IP地址,就是一直在变的,这个也就是限制所在了,但一是我们的IP不会那么快变,二是连接之后只要你不断开,IP变了也是可以继续使用的,三是warp的例外支持网段,我们可以尽可能的扩大例外的规模,来减少我们失去例外的机会。

那么接下里就是去百度搜一下IP地址,找到你本机目前的IP,然后在服务器的命令行里,输入:
warp-cli add-excluded-route xx.xx.xx.xx
其中xx.xx.xx.xx就是你的IP地址,假如你想加入一个IP网段范围来减小失效的概率,可以将其改为:
xx.xx.0.0/16
注意!!!:上面这个网段,只是一个示例,表示从xx.xx.0.1到xx.xx.255.254之间的所有IP地址。在使用时,请确保里你理解这其中的风险以及网段的具体意义。(当然,允许这些IP访问不代表你的原本的其他安全鉴定会失效,这里只是针对warp的限制例外)
在完成上述步骤之后,可以运行下面的代码来开启warp(再次注意:如果你没添加上述规则,你的ssh将无法再连接,你只能通过云服务提供商提供的方法来连接了):
warp-cli connect
开启成功后,可以使用下面的代码来检查是否真正启动:
curl https://www.cloudflare.com/cdn-cgi/trace/
如果你看到返回的字段中,有warp=on存在,那就是开启成功了,下面你就可以继续请求访问ChatGPT了

一些可能遇到的问题和个人经验(及广告):

安装问题

在使用apt或apt-get安装时,返回了Unable to locate package的提示,那么可以考虑先运行
sudo apt update
来更新你的资源列表,但如果更新之后依然不行(比如我),那么cloudflare也提供了手动下载的方式,你可以在这个页面中找到对应的Linux版本的warp安装包:

https://pkg.cloudflareclient.com/packages/cloudflare-warp 找到对应的包之后,可以下载之后,利用注入scp之类的工具拷贝的服务器上,也可以复制下载链接,然后在服务器中运行:
curl -o name-of-your-file https://you-download-url
然后再进行安装,比如我的机器是Debian的,那么接下来我会运行:
sudo dpkg -i name-of-your-file
sudo apt-get install -f
然后就安装完毕了

IP网段问题

如果你还是觉得添加例外这个方法不靠谱,那么你可以继续研究cloudflare的zero trust方案,也欢迎你推出新的教程并告诉我,下面是对应的文档地址:

https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/use_cases/ssh/

服务器和专业上网(广告)

其实现在来说,影袜的方案,就我自己的经验来看,是非常容易被嗅探到的,尤其是那些常用的海外云服务,之前经常就是这个服务器我刚刚开好,安装好了影袜,结果没几分钟就被封了,而且是明显IP能ping通,但不论你怎么换端口也都不行了,但现在我用的方案,非常简单,无需安装额外的工具,而且只要IP能ping通,基本上就能稳定使用。那就是使用ssh自带的动态端口转发功能,只需要一行指令:
ssh -N -g -D 1088 username@xx.xx.xx.xx
即可在你本地的1088端口开放一个通道,如果你只是希望自己的本机使用,那么可以去掉-g指令,或者显式的声明127.0.0.1:1088,如果不加,则其实同个局域网的其他设备,也可以访问你的IP:1088来进行专业上网,比如手机上,可以直接用小火箭来访问,稍微修改下配置即可。

然后以Mac为例,则可以在自己的设置-网络-高级-你懂得里配置诸如socks或者http方案,以及例外域名或者PAC方案。当然,如果你想要方便的管理这些配置而不是每次都打开设置来操作,可以尝试下我开发的Mac App:

https://apps.apple.com/cn/app/proxyho/id6444635008?mt=12
当然ssh的方案,因为加密算法的原因,其实理论速度是会不如专门为了这个而生的影袜的,但个人感觉这些损耗可以忽略不计,毕竟安逸了许多

另外如果你看了本篇文章,希望选择服务器,那么我也推荐一下Vultr,我个人认为性价比还可以,而且它们的官网并不需要特殊的姿势就可以直接访问,也很友好,下面是我的推广链接,服务器都是按使用时长计费的,试用不满意,关了就好了。当然他们家用的人很多,那么就也会偶尔出现IP被ban的情况,但因为是按照时长收费的,遇到这种情况,关闭服务器再重新启动一个即可。

https://www.vultr.com/?ref=9418691-8H

对了,他们家还支持ipv6网络方案,也无需额外付费,只要勾选即可

解决SwiftUI中的透明view点击无法生效的问题

透明的view,无论是整体opacity设置为了0,还是颜色设置为了clear,在swiftUI中,默认的content shape (内容形状)就是0,所以点击事件是无法生效的,而要解决这个问题,我们只需要手动设置content shape即可 代码如下:
Color.clear
  .frame(width: 300, height: 300)
  .contentShape(Rectangle())     // 这里,当然上面的frame也有必要,如果frame为0,则肯定也无法触发点击
  .onTapGesture { print("tapped") } 

SwiftUI中如何自定义Navigation返回按钮

iOS15+

// 自定义的返回按钮,按照你的需求自己定制
struct NavBackButton: View {
    let dismiss: DismissAction
    
    var body: some View {
        Button {
            dismiss()
        } label: {
            Image("...custom back button here")
        }
    }
}

// 在要使用的view下,加入如下modifier
.navigationBarBackButtonHidden(true) // Hide default button
.navigationBarItems(leading: NavBackButton(dismiss: self.dismiss)) // Attach custom button
在引入了上述逻辑后,就会在child view中显示你的自定义的按钮,但同时,左滑返回的手势会失效,如果你需要保留这个手势,则还需要加入如下的代码:
// 让使用自定义返回按钮时,左滑返回的动画不失效,同时对child view是scrollview等也需要监听drag gesture的情况做了兼容
extension UINavigationController: UIGestureRecognizerDelegate {
    override open func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        return viewControllers.count > 1
    }

    // To make it works also with ScrollView
    public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        true
    }
}

iOS15之前

struct SampleDetails: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var btnBack : some View { Button(action: {
        self.presentationMode.wrappedValue.dismiss()
        }) {
            HStack {
            Image("ic_back") // set image here
                .aspectRatio(contentMode: .fit)
                .foregroundColor(.white)
                Text("Go back")
            }
        }
    }
    
    var body: some View {
            List {
                Text("sample code")
        }
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: btnBack)
    }
}

SwiftUI 如何让view强制支持横屏或竖屏

问题:有时候在编写APP时,我们会需要某个页面只支持手机的某个方向(并不是懒得适配,确信)。而在swiftUI中,要实现这个设定又变得更加复杂了,本篇文章就希望提供一个清晰且简单的解法,来支持这一点。

首先,我们需要swiftUI支持AppDelegate.

在非swiftUI中,这个文件是在创建项目的时候就会被系统一同创建的,但swiftUI并没有为我们创建这个文件,但这并不意味着swiftUI就无法连接到AppDelegate并作出响应

而其实建立连接也十分简单,首先,创建一个class,这里其实创建AppDelegate的任何方法都可以,但为了不重复赘述,我这里直接写好了后面屏幕方向设定会用到的方法

class AppDelegate: NSObject, UIApplicationDelegate {
static var orientationLock = UIInterfaceOrientationMask.portrait

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -&gt; UIInterfaceOrientationMask {
return AppDelegate.orientationLock
}
}

这个设定,是将APP支持的屏幕方向设置了只支持竖屏,当然,如果你的APP也仅仅想做到这一步,那其实在设置里也可以直接做到,不用专门写一个class

接下来,在某个我们希望只支持横屏的view中,只要写下如下代码,即可切换到横屏

import SwiftUI

struct DestinationView: View {

var body: some View {
Group {

Text("Hello")

}.onAppear {
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
.onDisappear {
DispatchQueue.main.async {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
}
}

要注意的是,UIDevice中设定Value时要使用rawValue,否则会报错,而在onDisappear方法中,要使用异步来保证当前view消失时,不会因为之前的view的屏幕朝向不同而报错

Docker container容器内访问宿主机host服务

问题:docker的container内,本身是一个微小的主机,那么请求127.0.0.1或者localhost,自然是请求到了container本身的网络,而无法抵达宿主机。

在以往,需要各位去手动寻找docker创建的docker0 bridge来访问宿主机网络,但在今天,docker已经提供了非常优雅的解决办法了

Linux:

Docker版本高于v20.10(2020年12月4日更新)

在启动docker时,加入如下语句
--add-host=host.docker.internal:host-gateway
而在container内,可以直接请求host.docker.internal:PORT,来获取宿主机上提供的各种服务

如果使用了Docker Compose,则应该将下面的句子加入container的声明中:

extra_hosts:
- "host.docker.internal:host-gateway"

Mac和Windows:

Docker版本高于v18.03(2018年3月21日更新)

直接在container内使用host.docker.internal:PORT来访问宿主机服务即可

对于Mac上Docker版本低于上述版本的:

Mac Docker版本v17.12到v18.02: 使用docker.for.mac.host.internal

Mac Docker版本v17.06到v18.11: 使用docker.for.mac.localhost

对于更低版本的docker,只能使用老旧的方法了,这里不再多做赘述

参考:https://stackoverflow.com/a/43541732

SwiftUI 遇到Simultaneous accesses to XXX, but modification requires exclusive access的解决办法

本质上这个问题是一个系统bug,swiftUI中,如果你的List下不完全是依靠Foreach加载的core data的数据的话(即,你在list中添加了其他的View,例如自己写了个自定义的Title等),而你又提供了删除或者移动row的方法时,就会出现这种错误。

这是由于swiftUI Foreach的onDelete的方法没有兼容上面所述的情况造成的

目前可以使用的的解决办法是这样的:
viewContext.perform {
            offsets.map { molts[$0] }.forEach(viewContext.delete)

            do {
                try viewContext.save()
            } catch {
                viewContext.rollback()
                userMessage = "\(error): \(error.localizedDescription)"
                displayMessage.toggle()
            }
}
在你的操作方法外,再套一层,调用viewContext.perform即可

使用SSH来连接远程服务器的MySQL

本文要解决什么问题:当远程服务器无法开放MySQL端口或者无权限操作远程服务器的安全组时,我们应该如何在本地连接到远程的MySQL并进行操作

本文基础要求:远程服务器至少开放了SSH端口(通常默认情况下端口号为22),如果未开放22端口,则本文所述方法并不可用。

本文方法总结:利用SSH自带的端口映射功能,建立一个tunnel,将远程服务器上的MySQL端口映射到本地的端口上,从而使得本地客户端可以直接连接

关于如何使用SSH,以及如何更简单的使用SSH,可以参考另一篇文章

当SSH已经准备就绪,打开Terminal,输入如下指令


ssh -L [local port]:[database host]:[remote port] [username]@[remote host]

ssh -L [本地端口]:[数据库所在地址]:[数据库远程端口] [远程服务器用户名]@[远程服务器地址]

*如果你不用同时登陆远程服务器进行操作,可以加上-N指令

以上指令唯一需要解释的就是数据库所在地址,如果远程服务器本身就是MySQL服务器,那么可以直接填127.0.0.1, 而如果远程服务器本身还是一个跳板,则应该输入在远程服务器内可以访问到MySQL的地址。

示例如下:

ssh -N -L 3310:127.0.0.1:3306 root@MyRemote

以上的命令会将远程服务器监听3306端口的MySQL服务,映射到本地的3310端口上,之后可以直接使用MySQL客户端在命令行里登录,或者使用pma等数据库操作工具

mysql -u root -h 127.0.0.1 -P 3310 -p

SwiftUI CoreData entity/实体更新导致无法预览的问题

最近改动了某个entity的类型,取消了它的继承,preview突然就停止工作了,一直提示crash,直接调用以下方法,清除之前的preview设定即可,因为preview里还保存了旧的设定,与新设定冲突,导致preview crash

xcrun simctl --set previews delete all

如何在SwiftUI中给字体加粗

swiftUI的文本设置,给了非常简单易懂的字体选择器,而如果想给字体加粗,该如何实现呢

这里就可以这样设置

TextField("Name", text: $name)
.font(Font.headline.weight(.light))

而可以选择的字体大小的,有如下的从小到大的名称

.caption
.footnote
.subheadline
.callout
.body
.headline
.title
.largeTitle

而字体粗细的选择,则有如下从细到粗的字段可选

.ultralight
.thin
.light
.regular
.medium
.semibold
.bold
.heavy
.black