阅读视图

发现新文章,点击刷新页面。

Xcode 26还没有适配SceneDelegate的app建议尽早适配

Xcode 26之前不需要多窗口的很多app没有适配SceneDelegate,升级到Xcode 26后运行没有问题,但是控制台有以下输出:

`UIScene` lifecycle will soon be required. Failure to adopt will result in an assert in the future.

UIApplicationDelegate 中的相关生命周期函数也有弃用标记:

/// Tells the delegate that the application has become active 
/// - Note: This method is not called if `UIScene` lifecycle has been adopted. 
- (void)applicationDidBecomeActive:(UIApplication *)application API_DEPRECATED("Use UIScene lifecycle and sceneDidBecomeActive(_:) from UISceneDelegate or the UIApplication.didBecomeActiveNotification instead.", ios(2.0, 26.0), tvos(9.0, 26.0), visionos(1.0, 26.0)) API_UNAVAILABLE(watchos);

建议尽早适配

方案举例

以下是我的适配方案,供大家参考

  • 兼容iOS13以下版本;
  • app只有单窗口场景。

1. 配置Info.plist

Delegate Class Name和Configuration Name 可自定义

image.png

2. 配置SceneDelegate

  • 创建SceneDelegate class 类名要和Info.plist中配置一致

image.png

  • appDelegate中实现代理
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options  API_AVAILABLE(ios(13.0)){
   //  name要和Info.plist中配置一致
  return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}

- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions  API_AVAILABLE(ios(13.0)){
  // 释放资源,单窗口app不用关注
}

3. 新建单例 AppLifecycleHelper 实现AppDelegate和SceneDelgate共享的方法

  • iOS 13 及以上需要在scene: willConnectToSession: options: 方法中创建Window,之前仍然在 didFinishLaunchingWithOptions:

AppDelegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [AppLifecycleHelper sharedInstance].launchOptions = launchOptions;
     // ... 自定义逻辑
    if (@available(iOS 13, *)) {
 
    } else {
        [[AppLifecycleHelper sharedInstance] createKeyWindow];
    }
}

SceneDelgate:

URL冷启动APP时不调用openURLContexts方法,这里保存URL在DidBecomeActive处理

- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions  API_AVAILABLE(ios(13.0)){
    [[AppLifecycleHelper sharedInstance] createKeyWindowWithScene:(UIWindowScene *)scene];
    // 通过url冷启动app,一般只有一个url 
    for (UIOpenURLContext *context **in** connectionOptions.URLContexts) {
        NSURL *URL = context.URL;
        if (URL && URL.absoluteString.length > 0) {
            self.launchUrl = URL;
        }
    }
}

AppLifecycleHelper:

- (void)createKeyWindow {
    UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    [self setupMainWindow:window];
}

- (void)createKeyWindowWithScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0)) {
    UIWindow *window = [[UIWindow alloc] initWithWindowScene:scene];
    [self setupMainWindow:window];
}

- (void)setupMainWindow:(UIWindow *)window {
}
  • 实现SceneDelegate后appDelegate 中失效的方法

AppLifecycleHelper中实现,共享给两个DelegateClass

- (void)applicationDidBecomeActive:(UIApplication *)application {
    [[AppLifecycleHelper sharedInstance] appDidBecomeActive];
}

- (void)applicationWillResignActive:(UIApplication *)application {

}

- (void)applicationDidEnterBackground:(UIApplication *)application {

}

- (void)applicationWillEnterForeground:(UIApplication *)application {

}
  
 /// URL Scheme
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, **id**> *)options {

}

/// 接力用户活动
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<**id**<UIUserActivityRestoring>> * _Nullable))restorationHandler {

}

/// 快捷方式点击
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL))completionHandler API_AVAILABLE(ios(9.0)) {
}

SceneDelegate部分代码示例:


- (void)sceneDidBecomeActive:(UIScene *)scene  API_AVAILABLE(ios(13.0)){
    [[AppLifecycleHelper sharedInstance] appDidBecomeActiveWithLaunchUrl:self.launchUrl];
    // 清空冷启动时的url
    self.launchUrl = nil;
}

这个方法总结下来就是求同存异,由Helper提供SceneDelegate与AppDelegate相同或类似的方法,适合单窗口、且支持iOS 13以下的app;

另外注意URL Scheme冷启动app不会执行openURL需要记录URL,在合适的时机(一般是DidBecomeActive)处理。

swift中的知识总结(一)

一、associatedtype的用法 在swift中,泛型T是一个非常强大的特性,它允许我们编写灵活且可复用的代码。而当我们在 协议(Protocol) 中需要使用泛型时,associatedtyp

GetX 状态管理实践

下面内容只关注 GetxController / GetBuilder / Obx / 局部状态组件这些部分。


GetX 状态管理实践说明

本文介绍在项目中如何使用 GetxControllerGetBuilderObx / GetX 等组件来组织业务逻辑和控制 UI 更新。

GetxController 的角色与生命周期

GetxController 用来承载页面或模块的业务状态与逻辑,通常搭配 StatelessWidget 使用,无需再写 StatefulWidget。

  • 常用生命周期方法:
    • onInit:Controller 创建时调用,做依赖注入、初始请求、订阅等。
    • onReady:首帧渲染后调用,适合做需要 UI 已经渲染的操作(弹窗、导航等)。
    • onClose:Controller 销毁时调用,用于取消订阅、关闭 Stream、释放资源。

推荐习惯:

  • 把原来写在 StatefulWidget initState / dispose 里面的逻辑迁移到 Controller 的 onInit / onClose 中,UI 层尽量保持“傻瓜视图”。

GetX 中的两种状态管理方案

GetX 内置两类状态管理方式:简单状态管理(GetBuilder)与响应式状态管理(Obx / GetX)。

方案一:简单状态管理(GetBuilder + GetxController)

适用场景:不想使用 Rx 类型(.obs),希望显式控制刷新时机。

  • 写法示例:

    class CounterController extends GetxController {
      int count = 0;
    
      void increment() {
        count++;
        update(); // 手动触发使用该 controller 的 GetBuilder 重建
      }
    }
    
    class CounterPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final controller = Get.put(CounterController());
    
        return Scaffold(
          body: Center(
            child: GetBuilder<CounterController>(
              builder: (c) => Text('Count: ${c.count}'),
            ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: controller.increment,
          ),
        );
      }
    }
    
  • 特点:

    • 无需 .obs,状态是普通字段。
    • 只有调用 update() 的时候,使用该 Controller 的 GetBuilder 才会重建。
    • 适合页面级、大块 UI、不太频繁刷新场景。

方案二:响应式状态管理(Obx / GetX + Rx)

适用场景:已经在使用 .obs,或希望局部 UI 随状态变化自动刷新。

  • 写法示例:

    class CounterController extends GetxController {
      var count = 0.obs;
    
      void increment() => count++;
    }
    
    class CounterPage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final controller = Get.put(CounterController());
    
        return Scaffold(
          body: Center(
            child: Obx(() => Text('Count: ${controller.count}')),
            // 或
            // child: GetX<CounterController>(
            //   builder: (c) => Text('Count: ${c.count}'),
            // ),
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: controller.increment,
          ),
        );
      }
    }
    
  • 特点:

    • 变量通过 .obs 变为 Rx 类型(如 RxInt、RxString)。
    • 一旦值变化,Obx / GetX 所在的小部件自动重建,无需写 update()
    • 适合高频、小区域更新,如计数器、进度、网络状态、Switch 等。

两种方案的混用

在同一个项目、同一个 Controller 中,可以同时使用:

  • 一部分状态使用普通字段 + GetBuilder
  • 一部分状态使用 .obs + Obx / GetX

经验规则:

  • 页面大块区域、刷新不频繁 → 优先 GetBuilder
  • 小范围、高频刷新 → 优先 Obx / GetX

GetBuilder 的生命周期回调

GetBuilder 本质上是一个 StatefulWidget,内部有完整的 State 生命周期,对外通过参数暴露部分回调:[1]

  • 常用回调参数:
    • initState:对应 State.initState,适合调用 Controller 方法、发请求等。
    • didChangeDependencies:父级依赖变化时触发,用得不多。
    • didUpdateWidget:父组件重建、参数改变时触发。
    • dispose:组件销毁时触发,适合释放本地资源。

示例:

GetBuilder<CounterController>(
  initState: (_) {
    // widget 创建时执行
  },
  dispose: (_) {
    // widget 销毁时执行
  },
  builder: (c) => Text('Count: ${c.count}'),
);

建议:

  • 页面 /模块的“生命周期逻辑”优先放在 Controller.onInit/onClose
  • 某个局部 Widget 有特别的创建 / 销毁逻辑时,再使用 GetBuilder 的 initState / dispose

局部状态组件:ValueBuilder 与 ObxValue

对于“只在一个小部件内部使用”的临时状态,可以使用局部状态组件,而不必放入 Controller:

  • ValueBuilder(简单本地状态):
    dart ValueBuilder<bool>( initialValue: false, builder: (value, update) => Switch( value: value, onChanged: update, // update(newValue) ), );

  • ObxValue(本地 Rx 状态):

    ObxValue<RxBool>(
      (data) => Switch(
        value: data.value,
        onChanged: data, // 相当于 (v) => data.value = v
      ),
      false.obs,
    );
    

使用建议:

  • 仅在该 Widget 内使用且与全局业务无关的状态 → 用 ValueBuilder / ObxValue
  • 会被多个 Widget 或页面共享的状态 → 放入 GetxController,再用 GetBuilder / Obx 监听。

快速选型表

需求场景 状态写法 UI 组件 刷新方式
不想用 Rx,页面级 / 大块区域 普通字段 GetBuilder 手动 update()
已使用 .obs,局部自动刷新 .obs(RxXX 类型) Obx / GetX 值变化自动刷新
单个小 widget 内部的临时简单状态 普通字段 ValueBuilder 调用 updateFn
单个小 widget 内部的临时响应式状态 .obs ObxValue 值变化自动刷新

在这种“页面加载时请求 API”的需求里,推荐把请求放在 GetxController 的生命周期 里做,而不是放在页面 Widget 里。

方案一:在 onInit 里请求

适合“只要创建了这个 Controller(进入页面)就立刻请求”的场景。

class ArticleController extends GetxController {
  int pageCount = 0;              // 简单状态
  var likeCount = 0.obs;          // 响应式状态
  var isFavorite = false.obs;
  var loading = false.obs;        // 加载状态
  var article = Rxn<Article>();   // 文章详情

  @override
  void onInit() {
    super.onInit();
    increasePageCount();  // 原来的逻辑
    fetchArticle();       // 页面加载时请求 API
  }

  Future<void> fetchArticle() async {
    loading.value = true;
    try {
      final data = await api.getArticleDetail(); // 这里调用你的 repository / service
      article.value = data;
      // article 是 Rx,使用 Obx 的地方会自动刷新
      // 如果你有依赖简单状态的 GetBuilder,需要的话再调用 update()
      // update();
    } finally {
      loading.value = false;
    }
  }

  void increasePageCount() {
    pageCount++;
    update(); // 刷新 GetBuilder
  }

  void like() => likeCount++;
  void toggleFavorite() => isFavorite.toggle();
}

页面里依然混用 GetBuilder + Obx:

class ArticlePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final controller = Get.put(ArticleController());

    return Scaffold(
      appBar: AppBar(title: const Text('Article Detail')),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // 顶部浏览次数(简单状态)
          GetBuilder<ArticleController>(
            builder: (c) => Text('页面浏览次数:${c.pageCount}'),
          ),

          const SizedBox(height: 16),

          // 中间部分:加载中 / 内容(响应式状态)
          Obx(() {
            if (controller.loading.value) {
              return const CircularProgressIndicator();
            }
            final article = controller.article.value;
            if (article == null) {
              return const Text('暂无数据');
            }
            return Text(article.title); // 文章标题
          }),

          const SizedBox(height: 16),

          // 点赞 + 收藏(响应式状态)
          Obx(
            () => Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('点赞:${controller.likeCount}'),
                const SizedBox(width: 16),
                Icon(
                  controller.isFavorite.value
                      ? Icons.favorite
                      : Icons.favorite_border,
                  color: controller.isFavorite.value ? Colors.red : null,
                ),
              ],
            ),
          ),

          const SizedBox(height: 24),

          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: controller.increasePageCount,
                child: const Text('增加浏览次数 (GetBuilder)'),
              ),
              const SizedBox(width: 16),
              ElevatedButton(
                onPressed: controller.like,
                child: const Text('点赞 (Obx)'),
              ),
              const SizedBox(width: 16),
              ElevatedButton(
                onPressed: controller.toggleFavorite,
                child: const Text('收藏切换 (Obx)'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

方案二:在 onReady 里请求(需要等页面渲染后)

如果你的 API 请求需要在“首帧 UI 出来之后”再做,比如要先弹一个对话框提示用户,将请求放在 onReady

@override
void onReady() {
  super.onReady();
  fetchArticle(); // 首帧渲染完成后请求
}

不再建议的做法

  • 不建议再在页面的 initState 里请求,而是优先放到 GetxController.onInit / onReady,这样视图层更干净,也更符合 GetX 推荐的结构。

Swift Array的写时复制

众所周知Swift中Array是值类型,如果其中元素为值类型和引用类型,分别会发生什么? 相关验证方法 检查不同层次的地址 元素为引用类型 随便定义一个类,并创建列表1,然后直接赋值给列表2 用wit
❌