开源邀请归因系统 share-installs:自托管延迟深度链接匹配方案

前言 你有没有遇到过这种场景: 用户 A 分享了一个邀请链接给用户 B。用户 B 在手机上点击链接,跳转到 App Store 下载了 App。安装打开后——邀请码呢? 传统方案要么依赖复杂的 Deep Link 配置(Universal Link / App Links),要么需要接入 Firebase Dynamic Links 这类第三方服务。前者配置繁琐,后者有被 deprecate 的风险。 这就是我开源 share-installs 的原因。 什么是 share-installs? share-installs 是一个开源、自托管的 邀请码 & 延迟深度链接归因系统。 核心能力只有一句话:用户点击邀请链接并安装 App 后,首次启动时邀请码会自动识别并回填——即使点击时 App 尚未安装。 项目地址:github.com/ceeyang/share-installs ⭐ 核心功能:浏览器指纹 → 后端匹配 → App 启动自动回填 🖥️ 三端 SDK:Web(TypeScript)+ iOS(Swift)+ Android(Kotlin) 🐳 一键部署:Docker Compose / Kubernetes / 裸机 🔐 自托管:数据存在你自己的服务器上 工作原理 整个流程分为四个阶段: [阶段 1] 用户 A 分享邀请链接 │ ▼ [阶段 2] 用户 B 点击链接 → 你的落地页 │ JS SDK 上报浏览器指纹到后端 ▼ [阶段 3] 跳转 App Store / Play Store │ ▼ [阶段 4] 用户 B 安装并打开 App │ 移动 SDK 上报设备指纹 ▼ 后端匹配指纹 → 返回邀请码 │ ▼ App 自动填入邀请码 ✓ 关键技术点: ...

2026-06-01 · 3 min · 491 words · Cee Yang

Swift Debug Print

自定义 swift 控制台输出 // MARK: - Debug Print - public func print<T>(_ message: T, fileName: String = #file, methodName: String = #function, line: Int = #line) { #if DEBUG let printInfo = """ \n ================================================================================================================================================= 【File Name】: \(fileName.split("/").last ?? "") 【FunctionName】: \(methodName) 【Fuction Line】: \(line) 【Print Time】: \(Date()) 【 Message 】: \(message) ================================================================================================================================================= \n """ print(printInfo, separator: "", terminator: "") #endif }

2019-08-10 · 1 min · 62 words · Cee Yang

UITextView+Placeholder

Swift UITextView 扩展 Placeholder import UIKit fileprivate var kTextViewPlaceholderLabel : Int = 0x2019_00 fileprivate var kTextViewPlaceholder : Int = 0x2019_01 fileprivate var kTextViewPlaceholderColor : Int = 0x2019_02 fileprivate var kTextViewPlaceholderFont : Int = 0x2019_03 fileprivate var kTextViewPlaceholderKeys : Int = 0x2019_04 extension UITextView { /// 占位符 var x_placeholder: String { get { if let placeholder = objc_getAssociatedObject(self, &kTextViewPlaceholder) as? String { return placeholder } else { return "" } } set { objc_setAssociatedObject(self, &kTextViewPlaceholder, newValue, .OBJC_ASSOCIATION_RETAIN) x_placeholderLabel.text = newValue } } /// 占位符颜色 var x_placeholderColor: UIColor { get { if let placeholderColor = objc_getAssociatedObject(self, &kTextViewPlaceholderColor) as? UIColor { return placeholderColor } else { return UIColor.subTitleColor } } set { objc_setAssociatedObject(self, &kTextViewPlaceholderColor, newValue, .OBJC_ASSOCIATION_RETAIN) x_placeholderLabel.textColor = newValue } } /// 占位符字体 var x_placeholderFont: UIFont { get { if let placeholderFont = objc_getAssociatedObject(self, &kTextViewPlaceholderColor) as? UIFont { return placeholderFont } else { return UIFont.systemFont(ofSize: 12) } } set { objc_setAssociatedObject(self, &kTextViewPlaceholderColor, newValue, .OBJC_ASSOCIATION_RETAIN) x_placeholderLabel.font = newValue } } /// 占位符 标签 @IBInspectable var x_placeholderLabel: UILabel { get { var _placeholderLabel = UILabel(font: self.font ?? UIFont.systemFont(ofSize: 12), color: .subTitleColor, alignment: .left) if let label = objc_getAssociatedObject(self, &kTextViewPlaceholderLabel) as? UILabel { _placeholderLabel = label } else { objc_setAssociatedObject(self, &kTextViewPlaceholderLabel, _placeholderLabel, .OBJC_ASSOCIATION_RETAIN) } addPlaceholderLabelToSuperView(label: _placeholderLabel) return _placeholderLabel } set { objc_setAssociatedObject(self, &kTextViewPlaceholderLabel, newValue, .OBJC_ASSOCIATION_RETAIN) addPlaceholderLabelToSuperView(label: newValue) } } /// 是否需要添加占位符到父视图 fileprivate var x_placeHolderNeedAddToSuperView: Bool { get { if let isAdded = objc_getAssociatedObject(self, &kTextViewPlaceholderKeys) as? Bool { return isAdded } return true } set { objc_setAssociatedObject(self, &kTextViewPlaceholderKeys, newValue, .OBJC_ASSOCIATION_RETAIN) } } /// 添加占位符到父视图 /// /// - Parameter label: 占位符 标签 fileprivate func addPlaceholderLabelToSuperView(label: UILabel) { guard x_placeHolderNeedAddToSuperView else { return } x_placeHolderNeedAddToSuperView = false NotificationCenter.default.addObserver(self, selector: #selector(x_textChange(noti:)), name: UITextView.textDidChangeNotification, object: nil) addSubview(label) label.snp.makeConstraints { (make) in make.edges.equalToSuperview().inset(UIEdgeInsets(top: 7, left: 2, bottom: 0, right: 0)) } } /// 编辑事件 @objc fileprivate func x_textChange(noti: NSNotification) { let isEmpty = text.isEmpty print("text:\(String(describing: text))\nisEmpty:\(isEmpty)") x_placeholderLabel.text = isEmpty ? x_placeholder : "" x_placeholderLabel.isHidden = !isEmpty } }

2019-08-08 · 2 min · 316 words · Cee Yang

iOS UITabBarController 嵌套 UINavigationController

iOS UITabBarController 嵌套 UINavigationController 本文处理两个问题: 1. 自定义导航按钮后, 滑动返回手势失效问题 2. TabBar 嵌套 NavigationBar 的 TabBar 显示隐藏错乱等问题 主要思路 使用 UITabBarController 作为 Window 的 rootViewController 自定义 UINavigationController 控制器, 全局控制 tabbar 的显示与隐藏 添加自定义控制器为 UINavigationController 的 root 控制器 UITabBarController 添加每个 UINavigationController 控制器 直接上代码 自定义的 UITabBarController : import UIKit class HomeTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() createChildController() } /// 通过自定义方法添加所有子控制器 func createChildController() { addChildVC(childVc: HomeViewController(), title: "首页", image: "IMG_Home", selectedImage: "IMG_Home_Selected") addChildVC(childVc: MoreViewController(), title: "应用", image: "IMG_More", selectedImage: "IMG_More_Selected") addChildVC(childVc: MineViewController(), title: "我的", image: "IMG_Mine", selectedImage: "IMG_Mine_Selected") } /// 自定义添加子控制器 func addChildVC(childVc: UIViewController, title:String, image: String, selectedImage: String) { let nav = HomeNavigationController(rootViewController: childVc) let normalImage = UIImage(named: image) let selectedImage = UIImage(named: selectedImage) nav.navigationItem.title = title nav.tabBarItem = tabbarItem(with: title, normalImage: normalImage!, selectedImage: selectedImage!) addChildViewController(nav) } /// 快捷创建 UITabBarItem func tabbarItem(with title: String, normalImage: UIImage, selectedImage: UIImage) -> UITabBarItem{ let image = normalImage.withRenderingMode(.alwaysOriginal) let _selectedImage = selectedImage.withRenderingMode(.alwaysOriginal) let tabBarItem = UITabBarItem(title: title, image: image, selectedImage: _selectedImage) tabBarItem.setTitleTextAttributes([NSAttributedStringKey.foregroundColor: UIColor.mainColor], for: .selected) tabBarItem.setTitleTextAttributes([NSAttributedStringKey.foregroundColor: UIColor.tabbarNormalColor], for: .normal) return tabBarItem } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } } //让图片和文字在 iPad 下仍然保持上下排列 extension UITabBar { override open var traitCollection: UITraitCollection { if UIDevice.current.userInterfaceIdiom == .pad { return UITraitCollection(horizontalSizeClass: .compact) } return super.traitCollection } } 自定义的 UINavigationController : ...

2018-09-18 · 2 min · 337 words · Cee Yang

5分钟带你看完 WWDC 2018

前言 一年一度的 WWDC(苹果全球开发者大会)于北京时间 6月5日 凌晨1点开幕。废话不多说,来看看这次WWDC 都有哪些亮点吧! iOS 12 和 ARKit 2.0 关键词:官方防沉迷最为致命 iOS 12 iOS 12 相较于 iOS 11 并没有太多UI上的变动,刚更新完 bate 版本的 iOS 12,完全感觉不到这是个新系统。 iOS 12 主要是对安全和性能的优化,iOS 12 在旧设备上的运行速度比 iOS 11更块,程序加载速度快了一倍。(PS:看来苹果并没有放弃旧设备) ARKit 2.0 Apple 与 皮克斯 合作开发了一种用于共享AR内容的新文件格式,新的 AR 格式名为 USDZ。 作为一个含着金苹果出生的新生儿,USDZ 一开始就得到了 Adobe Creative Cloud (包括 Photoshop CC、InDesign CC、Illustrator CC、Dreamweaver CC、Premiere Pro CC)套件的支持。 同时,面向开发者的开发套件 ARKit 则升级到了二代,主要提升了面部跟踪、渲染能力、3D 探测和共享体验等能力。 随后展示了一款名为 Measure 的 App,可使用AR查看物品大小。 最后为了演示新的 AR 能力和效果,苹果请来了乐高的创意总监来捧场。这是一个真实的乐高积木建筑物为基础,最多四个人可以用苹果 AR 应用进行游戏,可以在真实环境中模拟出各种虚拟的形象和建筑。 ...

2018-06-05 · 2 min · 387 words · Cee Yang

Xcode命令行工具管理

安装 xcode-select --install Xcode版本切换 显示当前使用的xocde版本 $ xcode-select --print-path 选择Xcode中的默认版本 $ sudo xcode-select -switch /Applications/Xcode.app

2018-05-05 · 1 min · 14 words · Cee Yang

Swift 4.2 新特性

private 权限扩大 在 Swift 4 中,extension 可以读取 private 变量了。 Swift 3 中,如果将主体函数的变量定义为 private,则其 extension 无法读取此变量,必须将其改为 filePrivate 才可以。 单向区间 单向区间是一个新的类型,主要分两种:确定上限和确定下限的区间。直接用字面量定义大概可以写成 …6和 2… 例如 let intArr = [0, 1, 2, 3, 4] let arr1 = intArr[...3] // [0, 1, 2, 3] let arr2 = intArr[3...] // [3, 4] 字符串改动 String 操作简化了 String 许多要通过 .characters 进行的操作,可以直接用 String 进行操作了。 例如: let greeting = "Hello, 😜!" // No need to drill down to .characters let n = greeting.count let endOfSentence = greeting.index(of: "!")! 新增 Substring 类型 swift 4 为字符串片段新增了一个叫 Substring 的类型。 当你创建一个字符串的片段时,会产生一个 Substring 实例。Substring 与 String 用法相同, 因为子串和原字符串共享内存,所以对子串的操作快速而且高效。 let greeting = "Hi there! It's nice to meet you! 👋" let endOfSentence = greeting.index(of: "!")! // 产生 Substring 实例 let firstSentence = greeting[...endOfSentence] // firstSentence == "Hi there!" // `Substring` 与 `String` 用法相同 let shoutingSentence = firstSentence.uppercased() // shoutingSentence == "HI THERE!" 但是要注意一个 Substring 保留从其生成的完整的 String值。 当您传递一个看似很小的 Substring 时,这可能导致意外的高内存开销。所以使用 Substring时,最好转化为 String. ...

2017-09-11 · 2 min · 321 words · Cee Yang

iOS11上手体验

WWDC2017于今天凌晨1点准时开始,说实话笔者等这一天等很久了。 ##以下是笔者的上手体验: 系统界面的更新 iMessage:通过iCloud将iMessage里的对话内容进行云端同步。 Camera:相机可以直接读取图片中的二维码,每张照片压缩率为此前的两倍,你会有更多空间存储照片,此外还可更好地分类,笔者测试了一下,拍一张照片是比以前用的内存小,各位内存吃紧的小伙伴有福利了。 Control Center:更多的功能将加入其中,改变音量大小等变为了模块式,界面优化明显。 Siri:通过深度学习,可以理解你的言语,结合上下文进一步知晓你的兴趣爱好等,笔者对iOS9的 Siri有点小失望,这一代的估计不错,不过笔者来得及没体验,留个各位同学自己上手体验吧。 Apple Music:可以与朋友一同分享音乐,包括微信,QQ。 App Store:经过重新设计,内容将通过“今日推荐”、“游戏”、“App”三个标签页进行展示。 Keyboard:内置键盘加入中国的拼音输入,并且新增单手操作模式。 Fiels:属于 iOS 的文件管理器。不过貌似没啥用,不能获取其他 APP 的沙盒文件。 ###凌晨看完发布会直播,眼睛已经涩得不行,只能去睡觉,第二天体验新版的 iOS11 系统了,一大早来公司就下载了新系统安装,刚开始还不适应,不过感觉也还行。使用中发现许多 bug,不过毕竟是测试版,就不多做评价。下面笔者带你走进 iOS11,至于是否更新测试版本各位同学自己决定咯。 系统界面的更新 顶部状态了的信号图标变了,变成安卓风格,个人觉得还是以前的小圆点好看,虽然笔者用的是表示信号强度的数字。以前锁屏界面的通知消息能够右滑清除,新版的系统好像不可以了。只能长安,或者重按,笔者用的 iPhone 7,带 3DTouch 的功能,重按后出现了上面的通知详情界面。上拉能显示历史通知消息,笔者这里把历史通知都删除了,看不到所以就没截图。不管是在通知消息上面右滑还是在空白处右滑都会打开相机。有点小小的不习惯啊。 进入主页的动画效果也有改变。 Camera:照相机 哈哈,相机已经可以直接读取图片中的二维码了,也就是说拿着相机就可以扫描其他二维码,虽然整个过程需要打开浏览器,再打开各个 APP,过程比较繁琐,但这进步也是值得称赞的。不过笔者跳转到微信后没任何反应,哈哈,看来又是 bug 。期待后期的使用了。等等,听说安卓的早就可以了?安卓同学请略过QAQ。 Control Center:控制中心 控制中心可以自定义菜单,如图,笔者选了一些功能。笔者这里只提几点:点击 WiFi 后只能打开与关闭,重按也只能打开一个更多的页面,并不能在控制中心选择 WiFi,蓝牙也是如此。看以前暴露的视频,WiFi 和蓝牙是可以在控制中心选择的,但笔者没测试出来,或许是 bug 来着。字体大小按钮,可以将全局的字体整体放大缩小,拿 iPhone 当老年机的话笔者感觉这个功能会有点用。辅助功能里面新增几个选项:SOS,Pay,重新启动,SOS 创建临时急救方案,点一下自动拨打急救电话,貌似以前也有,现在提到控制中心了而已。其他几个也不多说。 计算器页面也重新做了次,各位同学不知意下如何。 Apple Music:苹果音乐 ...

2017-06-08 · 1 min · 80 words · Cee Yang

App热修复详解

App Hotfix(热修复)详解 定义: 从广义的角度理解,大家都比较认同 Hotfix 是在移动端不需要重新发版,通过在线更新对版本 Bug 的修复。 现在比较流行的热修复技术分为三种: 一、使用JSPatch进行热修复: Objective-C 是动态语言,OC上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名和方法名反射得到相应的类和方法,也可以替换某个类的方法为新的实现,还可以新注册一个类,为类添加方法替换方法,通过这些即可实现动态修复 APP 技术。 JSPatch是一个在Github上的开源项目,JSPatch下载地址。JSPatch 的实现主要是通过 Objective-C的 runtime 原理,即利用JS传递字符串给OC,OC通过 Runtime 接口调用和替换OC方法。具体实现原理请参考作者的帖子:JSPatch 实现原理详解 (整改版)。 OC转JS工具,具体实现参考该博客 大体实现思路如下: 首先,开发者提供热修复脚本; 其次,要将脚本上传到后台,后台需要提供上传的操作页面; 然后,终端设备每次运行后请求获取最新的脚本文件; 最后,解析脚本文件,调用 JSPatch 引擎,执行脚本文件并修复; 1、开发者提供热修复脚本: 脚本的书写: 脚本书写也很简单,先用Objective-C将要需要更改的代码改好,然后根据需要修改的代码更改成js代码即可,具体书写方法请参照 JSPatch使用说明,或者使用上面提供的OC转JS工具。 例如: OBjective-C代码,这里是需要修改的内容。 @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; /** 该代码在上线后的项目里面并没有 * 即在 APP 上线后又临时修改的。 * 添加或者修改我们需要改动代码,如无需改动,该方法不变,此处拿修改标题做测试,可以做很堵其他操作; */ self.title = @"Welcome to use JSPatchDemo"; } /** * 省略其他代码 */ @end javascript代码:JS 属于链式语法,相信很多人都会,可以查看 JSPatch 的语法规则自己写,也可以通过OC转JS工具将上面的代码转换成JS 代码 ...

2017-01-05 · 1 min · 163 words · Cee Yang

快速添加圆角和描边

前言 对于习惯使用Storyboard的人来说,设置圆角、描边是一件比较蛋疼的事,因为苹果没有在xcode的Interface Builder上直接提供修改控件的圆角,边框设置。 我们来说说如何对某个控件进行圆角、描边处理: 初级 对于一个初学者来说,如果要进行某个控件的圆角、描边设置,就要从Storyboard关联出属性,然后再对属性进行代码处理。 如下代码: self.myButton.layer.cornerRadius = 20; self.myButton.layer.masksToBounds = YES; self.myButton.layer.borderWidth = 2; self.myButton.layer.borderColor = [UIColor blackColor].CGColor; 这样不仅需要Storyboard关联出属性,还要写一堆代码对属性进行设置,不得不说实在麻烦~ 中级 更聪明的做法是使用Storyboard提供的Runtime Attributes为控件添加圆角描边。 选中控件,然后在Runtime Attributes框中输入对应的Key与Type与Value,这样程序在运行时就会通过KVC为你的控件属性进行赋值。(不仅仅是圆角、描边~) 如下图 设置圆角、描边的Key为: layer.borderWidth layer.borderColorFromUIColor layer.cornerRadius clipsToBounds 我这次在测试时, 这样做不用关联出属性,但是需要输入大串字符串,也是不够方便。 高级 创建UIView的分类,使用IBInspectable+ IB_DESIGNABLE关键字: #import <UIKit/UIKit.h> IB_DESIGNABLE @interface UIView (Inspectable) @property(nonatomic,assign) IBInspectable CGFloat cornerRadius; @property(nonatomic,assign) IBInspectable CGFloat borderWidth; @property(nonatomic,assign) IBInspectable UIColor *borderColor; @end #import "UIView+Inspectable.h" @implementation UIView (Inspectable) -(void)setCornerRadius:(CGFloat)cornerRadius{ self.layer.masksToBounds = YES; self.layer.cornerRadius = cornerRadius; } -(void)setBorderColor:(UIColor *)borderColor{ self.layer.borderColor = borderColor.CGColor; } -(void)setBorderWidth:(CGFloat)borderWidth{ self.layer.borderWidth = borderWidth; } - (CGFloat)cornerRadius{ return self.layer.cornerRadius; } - (CGFloat)borderWidth{ return self.layer.borderWidth; } - (UIColor *)borderColor{ return [UIColor colorWithCGColor:self.layer.borderColor]; } @end 附上:GitHub地址 ...

2016-12-01 · 1 min · 121 words · Cee Yang