【问题标题】:Passing data between classes / asynchronous requests / iOS在类/异步请求/iOS之间传递数据
【发布时间】:2023-03-18 10:03:01
【问题描述】:

我正在将我的应用程序从同步 HTTP 请求转换为异步 HTTP 请求,并且遇到了一个问题,看起来需要对应用程序如何处理其数据进行大量修改。让我试着解释一下

以前是这样的:

-Class1Class2Class3 都是 UIViewController 的子类 - 助手类 -内容展示类

他们做的事情大相径庭,但共同的特点是他们与助手类的交互。它们以多种不同方式从用户那里收集请求的详细信息,然后最终将请求发送到帮助程序类。
当它同步完成时,助手类将返回数据。然后每个类将解释数据(XML 文件)并通过 segue 将它们传递给内容显示类

大体上是这样的:

第一类:

//Get user input
SomeData *data = [helperclass makerequest];
id vcData = [data process];
[self performSegueWithIdentifier:@"segueIdentifier"];
---
- (void)prepareForSegue:(UIStoryboardSegue *)segue
{
    DestinationViewController *destination = (DestinationViewController *)segue.destinationViewController;
    destination.data = vcData;
}

内容展示类:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.data presentdata];
}

现在看起来像这样

我通过首先使其与 Class1 一起工作来解决此问题,以便将修复程序部署到 class2 和 class3。所以 class1 和 helper 现在像这样交互

第一类:

//Get user input
SomeData *data = [helperclass makerequestWithSender:self];
id vcData = [data process];
[self performSegueWithIdentifier:@"segueIdentifier"];
---
- (void)prepareForSegue:(UIStoryboardSegue *)segue
{
    DestinationViewController *destination = (DestinationViewController *)segue.destinationViewController;
    destination.data = vcData;
} 

现在我面临的最大问题是如何将 helperclass 中的数据返回到Class1。我设法让它工作了

(void)makeRequestWithSender:(Class1*)sender
{
  [NSURLConnection sendAsynchronousRequest:...
     {
        [sender sendData:data];
     }
}

但是,当我将其推广到其他 2 个 GUI 类别时,这将构成我遇到困难的请求。我的第一个想法是设置sender:(id),但在[sender sendData:data] 行失败,告诉我id 没有sendData: 或类似方法。

希望我在这里不是太含糊,你们可以提供帮助。如果需要,我将能够发布代码 sn-ps,但现在任何人都可以就如何构造此请求的代码提供更好的建议吗?

【问题讨论】:

    标签: iphone ios objective-c ipad oop


    【解决方案1】:

    您基本上想使用“观察者模式”或(可能)稍微更改的设置,因此您可以使用委托。

    观察者模式

    您通过NSNotificationCenterNSNotifications 获得机制。您的 3 个不同的 UIViewController 子类每个都订阅特定的 NSNotification,并且您通过 NSNotificationCenter 发布通知来通知它们。

    以下代码是如何在视图控制器子类中解决问题的示例:

    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
        // subscribe to a specific notification
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(doSomethingWithTheData:) name:@"MyDataChangedNotification" object:nil];
    }
    - (void)viewWillDisappear:(BOOL)animated {
        [super viewWillDisappear:animated];  
        // do not forget to unsubscribe the observer, or you may experience crashes towards a deallocated observer
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
    ...
    - (void)doSomethingWithTheData:(NSNotification *)notification {
        // you grab your data our of the notifications userinfo
        MyDataObject *myChangedData = [[notification userInfo] objectForKey:@"myChangedDataKey"];
        ...
    }
    

    在你的助手类中,数据改变后你必须通知观察者,例如

    -(void)myDataDidChangeHere {
        MyDataObject *myChangedData = ...;
        // you can add you data to the notification (to later access it in your viewcontrollers)
        [[NSNotificationCenter defaultCenter] postNotificationName:@"MyDataChangedNotification" object:nil userInfo:@{@"myChangedDataKey" : myChangedData}];
    }
    

    通过@protocol

    假设您的所有 UIViewController 子类都驻留在父视图控制器中,您可以在帮助器类中实现协议并使父视图控制器成为委托。然后父viewcontroller可以通过消息通知子uiviewcontroller。

    您的辅助类声明可能如下所示(假设为 ARC):

    @protocol HelperDelegate;
    
    @interface Helper : NSObject
    
    @property (nonatomic, weak) id<HelperDelegate> delegate;
    
    ...
    @end
    
    @protocol HelperDelegate <NSObject>
    
    -(void)helper:(Helper *)helper dataDidChange:(MyDataObject*)data;
    
    @end
    

    在助手实现中,您将通过以下方式通知委托:

    ...
    if ([self.delegate respondsToSelector:@selector(helper:dataDidChange:)]) {
        [self.delegate helper:self dataDidChange:myChangedDataObject];
    }
    ...
    

    您的父视图控制器需要成为助手类的委托并实现其协议;粗略的草图,在声明中

    @interface ParentViewController : UIViewController <HelperDelegate>
    

    以及短版的实现

    // you alloc init your helper and assign the delegate to self, also of course implement the delegate method
    
    -(void)helper:(Helper *)helper dataDidChange:(MyDataObject*)data {
        [self.myCustomChildViewController doSomethingWithTheNewData:data];
    }
    

    另外..

    您可能会问自己更喜欢哪种方法。两者都是可行的,主要区别在于,通过观察者模式,您可以“一次”通知更多对象,而协议只能有一个委托,并且如果需要,必须转发消息。有很多关于利弊的讨论。我建议您在下定决心后阅读它们(抱歉,没有足够的声誉来发布两个以上的链接,所以请在 stackoverflow 上搜索)。如果有不清楚的地方,请询问。

    【讨论】:

    • 观察者模式被证明是完美的。谢谢!
    【解决方案2】:

    这里有一些合理的想法。详细说明/添加我的意见:

    首先,哪个对象应该告诉下载者 (HelperClass) 开始下载?我的做法是在将呈现数据的视图控制器中执行此操作。所以我通常在 segue 之后启动网络请求(比如在所提供的 vc 的 viewWillAppear: 中),而不是之前。

    接下来,当一个类需要执行为另一个类提供的代码时,我首先考虑使用block 是否有意义。很多时候(并非总是)块比委托、通知、KVO 等更有意义并提供更易读的代码。例如,我认为 NSURLConnection 完成比委托更适合块。 (Apple 有点同意,介绍了+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler)。

    所以我的应用模式是这样的:

    // Class1.m
    
    // when user has completed providing input
    ...
    // don't do any request yet.  just start a segue
    [self performSegueWithIdentifier:@"ToContentDisplayClass" sender:self];
    ...
    
    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    
        // don't do a request yet, just marshall the data needed for the request
        // and send it to the vc who actually cares about the request/result
    
        if ([segue.identifier isEqualToString:@"ToContentDisplayClass"]) {
    
            NSArray *userInput = // collect user input in a collection or custom object
            ContentDisplayClass *vc = segue.destinationViewController;
            vc.dataNeededForRequest = userInput;
        }
        ...
    

    然后在ContentDisplayClass.m中

    // this is the class that will present the result, let it make the request
    
    - (void)viewWillAppear:(BOOL)animated {
        [super viewWillAppear:animated];
    
        HelperClass *helper = [[HelperClass alloc]
                                  initWithDataNeededForRequest:self.dataNeededForRequest];
    
        // helper class forms a request using the data provided from the original vc,
        // then...
    
       [helper sendRequestWithCompletion:^(NSURLResponse *response, NSData *data, NSError *error) {
            if (!error) {
                // interpret data, update view
                self.label.text = // string we pulled out of data
            } else {
                // present an AlertView? dismiss this vc?
            }
        }];
    

    这取决于HelperClass 实现block 形式的NSURLConnection

    // HelperClass.m
    
    - (id)initWithDataNeededForRequest:(id)dataNeededForRequest {
       // standard init pattern, set properties from the param
    }
    
    - (void)sendRequestWithCompletion:(void (^)(NSURLResponse *, NSData *, NSError *))completion {
    
        NSURLRequest *request = ...
        // the stuff we need to formulate the request has been setup in init
        // use NSURLConnection block method
    
        [NSURLConnection sendAsynchronousRequest:request
            queue:[NSOperationQueue mainQueue]
            completionHandler:completion];
    }
    

    编辑 - 在开始网络请求之前进行 VC 转换有几个理由:

    1) 围绕成功案例构建标准行为:除非应用程序是关于测试网络连接,否则成功案例是请求有效。

    2) 应用程序的基本原则是响应式的,在用户操作后立即做一些明智的事情。因此,当用户执行某些操作来发起请求时,立即进行 vc 转换是好的。 (取而代之的是什么?一个微调器?)。新呈现的 UI 甚至可以通过在运行时为用户提供一些新的东西来减少请求的感知延迟。

    3) 当请求失败时,应用应该怎么做?如果应用程序并不真的需要请求有用,那么什么都不做是一个不错的选择,所以你想在新的 vc 上。更典型地,该请求是继续进行所必需的。 UI 也应该“响应”请求失败。典型的行为是呈现一个提供某种形式的“重试”或“取消”的警报。对于任何一种选择,UI 想要的位置都在新的 vc 上。重试更加明显,因为它总是在尝试获取数据时出现。对于取消,“响应”取消的方式是回到旧的 vc,vc 转换回来并不难看,这是用户刚刚要求的。

    【讨论】:

    • 这正是我希望听到的基本答案。我肯定把问题复杂化了。我会试试这种方法,让你知道!
    • 这本来可行,但我选择使用上面的观察者示例,因为如果请求抛出错误,您的方法将导致 2 次视图更改,这有点难看。不过谢谢!
    • 当然。在某种程度上,这是一个品味问题,但我认为有强大的 UI 规范可以进行快速的 ui 转换。失败时的过渡实际上是一件正确的事情,而不是 - imo - 丑陋。将进行编辑以添加理由。
    【解决方案3】:

    我不是 100% 清楚您现在如何处理数据,但是要将您的数据更改为异步调用,我会使用块。例如,您当前的同步代码如下:

    //Get user input
    data = [helperclass makerequest]
    sendData = [data process]
    

    会变成这样:

    //Get user input
    data = [helperclass makerequestWithSuccess:^{
        sendData = [data process]
    }];
    

    使用成功块将允许您等待处理数据,直到 makerequest 完成。

    您的新 makerequest 函数现在看起来像这样:

    -(void)makerequestWithSuccess:(void (^)(void))success{
         // Put your makerequest code here
         // After your makerequest is completed successfully, call:
         success();
    }
    

    希望这会有所帮助!

    【讨论】:

      【解决方案4】:

      我不确定我是否正确理解了您的问题,但如果是这样的话:

      1. 异步启动任务 A。
      2. 当任务 A 成功完成后,获取其结果并启动输入为结果 A 的任务 B。
      3. 当任务 B 成功完成后,获取其结果并启动输入为结果 B 的任务 C。
      4. ...
      5. 成功完成后,开心,否则打印错误。

      代码示例如下所示:

      typedef (void)(^completion_block_t)(id result);
      
      -(void) asyncTaskA:(completion_block_t)completionHandler;
      -(void) asyncTaskBWithInput:(id)input completion:(completion_block_t)completionHandler;    
      -(void) asyncTaskCWithInput:(id)input completion:(completion_block_t)completionHandler;
      -(void) asyncSomethingWithCompletion:(completion_block_t)completionHandler;
      
      -(void) asyncSomethingWithCompletion:(completion_block_t)completionHandler
      {
          [self asyncTaskA:^(id resultA){
              if (![resultA isKindOfClass:[NSError class]]) {
                  [self asyncTaskBWithInput:resultA completion:^(id resultB){
                      if (![resultB isKindOfClass:[NSError class]]) {
                          [self asyncTaskCWithInput:resultB completion:^(id resultC) {
                              completionHandler(resultC);                         
                          }];
                      }
                      else {
                          completionHandler(resultB); // error;
                      }
                  }];
              }
              else {
                  completionHandler(resultA); // error
              }    
          }];
      }
      

      你会像这样使用它:

      [self asyncSomethingWithCompletion:^(id result){
          if ([result isKindOfClass:[NSError class]]) {
              NSLog(@"ERROR: %@", error);
          }
          else {
              // success!
              self.myData = result;
          }
      }];
      

      “延续”和错误处理使这有点令人困惑(而且 Objective-C 语法并没有真正增加可读性)。


      另一个第三方库支持的例子:

      同样的逻辑可以写成这样:

      -(Promise*) asyncTaskA;
      -(Promise*) asyncTaskBWithInput;    
      -(Promise*) asyncTaskCWithInput;
      -(Promise*) asyncSomething;
      
      - (Promise*) asyncSomething 
      {
          return [self asyncTaskA]
          .then(id^(id result) {
             return [self asyncTaskBWithInput:result];
          }, nil)
          .then(id^(id result) {
             return [self asyncTaskCWithInput:result];
          }, nil);
      }
      

      它的用法如下:

      [self asyncSomething]
      .then(^(id result) {
          self.myData = result;
          return nil;
      },  
      ^id(NSError* error) {
          NSLog(@"ERROR: %@", error);
          return nil;
      });
      

      如果你更喜欢后者,可以在 GitHub 上找到“Promise”框架:RXPromise - 我是作者 ;)

      【讨论】:

      • 我不是 100% 我把我最初的问题说得足够清楚。你对这个过程的描述有点宽泛,所以我不知道你的回答是否完全相关。以您在上面编写的 5. 点列表为例,但考虑到点 1. 可以源自 3 个不同的唯一类(UIViewControllers),并且在接收和处理数据时需要通知启动该过程的一个。在这种情况下,您的答案是否仍然相关?
      • 是的,100%。我做的唯一先决条件是从三个不同来源收集信息将按顺序进行。也就是说,不是并行运行。但是,当您向用户请求数据时,通常会出现这种情况。在我的示例中,这些是异步任务 A、B 和 C。这些是“自我”的方法并不重要。您可以与 viewController1、viewController2 等或其他任何东西交换“自我”。 异步任务可以是任何异步任务:网络访问、用户输入等。任务 C 的结果也可以简单地包括任务 A 和 B 的结果。
      • 还可以同时(和异步)运行 A、B 和 C。然后,当 所有 任务已成功完成时,调用站点可能会在另一个异步任务中继续处理这 3 个结果 - 然后在“顶部”(或发起)调用站点中处理其最终结果。我承认,这些示例有点“抽象”,对于开始使用异步模式的每个人来说,起初可能难以“掌握”。 ;)
      • 您可能会看到类似的question。答案可能还可以帮助您理解异步模式。它还详细解释了“承诺”的工作原理。
      【解决方案5】:

      我不确定我过去所做的是否与您的问题相关,但我所做的是创建一个下载类,该类具有 delegate 协议和单一方法:-(void)downloadFinished:(id) data

      任何需要获取异步数据的类,创建此下载类的实例,并将自身设置为delegate。我从connection:didFailWithError:connectionDidFinishLoading: 都给downloadFinished: 打电话。然后,在委托中实现该方法时,我检查数据的类是NSData 还是NSError,并评估该数据是否适合该类。

      【讨论】:

        猜你喜欢
        • 2020-08-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-05-14
        • 1970-01-01
        • 1970-01-01
        • 2013-05-17
        • 1970-01-01
        相关资源
        最近更新 更多