iOS学习——属性引用self.xx与_xx的区别

本文深入探讨了iOS开发中@property与@synthesize的关系及作用,解释了self.xx与_xx的区别,包括它们如何影响内存管理和对象引用。

       在iOS开发过程中,我们用@proprety声明一个属性后,在代码中我们可以用self.xx与_xx来获取到这个属性。但是一直有一个疑惑,那就是这两个之间有什么区别呢?最初我一直觉得这两个之间没什么区别的,直到有一次,我发现自己明明对声明的属性进行了赋值,但是在使用_xx引用时发现为nil,这才引起我的注意。所以,今天在这里对这个问题进行统一的一个说明和学习。

1 @property 与 @synthesize

       在说self.xx与_xx之前,我们先了解一下@property 以及 @synthesize之间的区别和联系,说到@property 以及 @synthesize,我们就不得不提到iOS中 成员变量和属性 之间的区别和联系了。

  接触iOS的人都知道,@property声明的属性默认会生成一个_类型的成员变量,同时也会生成setter/getter方法。 但这只是在iOS5之后,苹果推出的一个新机制。看老代码时,经常看到一个大括号里面定义了成员变量,同时用了@property声明,而且还在@implementation中使用@synthesize方法,就像下面的代码这样:

@interface ViewController ()
{
   // 1.声明成员变量
    NSString *myString;  
 }
 //2.在用@property
@property(nonatomic, copy) NSString *myString;  
@end

@implementation ViewController
//3.最后在@implementation中用synthesize生成set方法
@synthesize myString;   
@end

      其实,发生这种状况根本原因是苹果将默认编译器从GCC转换为LLVM(low level virtual machine),才不再需要为属性声明实例变量了。在没有更改之前,属性的正常写法需要 成员变量 + @property + @synthesize 成员变量 三个步骤。 

如果我们只写成员变量+ @property:

@interface GBViewController :UIViewController
{
    NSString *myString;
}
@property (nonatomic, strong) NSString *myString;
@end
//编译时会报警告:
Autosynthesized property 'myString' will use synthesized instance variable '_myString', not existing instance variable 'myString'

      但更换为LLVM之后,编译器在编译过程中发现没有新的实例变量后,就会生成一个下划线开头的实例变量。因此现在我们不必在声明一个实例变量。(注意:==是不必要,不是不可以==) 当然我们也熟知,@property声明的属性不仅仅默认给我们生成一个_类型的成员变量,同时也会生成setter/getter方法。在.m文件中,编译器也会自动的生成一个成员变量_myString。那么在.m文件中可以直接的使用_myString成员变量,也可以通过属性self.myString.都是一样的。注意这里的self.myString其实是调用的myString属性的setter/getter方法

  此外,如果我们再最新的代码中声明一个成员变量,如下代码所示,那么我们只是声明了一个成员变量,并没有setter/getter方法。所以访问成员变量时,可以直接访问name,也可以像C++一样用self->name来访问,但绝对不能用self.name来访问。

@interface MyViewController :UIViewController
{
    NSString *name;
}
@end

从Xcode4.4以后,即iOS的@property已经独揽了@synthesize的功能主要有三个作用:

  1. 生成了成员变量get/set方法的声明
  2. 生成了私有的带下划线的的成员变量因此子类不可以直接访问,但是可以通过get/set方法访问。那么如果想让定义的成员变量让子类直接访问那么只能在.h文件中定义成员变量了,因为它默认是@protected
  3. 生成了get/set方法的实现

值得注意的是:  

  • 如果已经手动实现了get和set方法(两个都实现)的话Xcode不会再自动生成带有下划线的私有成员变量了
  • 因为xCode自动生成成员变量的目的就是为了根据成员变量而生成get/set方法的,但是如果get和set方法缺一个的话都会生成带下划线的变量 

2 self.xx与_xx

      上面我们说到了属性与成员变量、@property 以及 @synthesize之间的联系与区别。同时,我们提到了self.xx和_xx的一点区别,其中self.xx是调用的xx属性的get/set方法,而_xx则只是使用成员变量_xx,并不会调用get/set方法。两者的更深层次的区别在于,通过存取方法访问比直接访问多做了一些其他的事情(例如内存管理,复制值等),例如如果属性在@property中属性的修饰符有retain,那么当使用self.xx的时候相应的属性的引用计数器由于生成了setter方法而进行加1操作,此时的retaincount为2。

  • 扩展:很多人觉得OC中的点语法比较奇怪,实际是OC设计人员有意为之。
  • 点表达式(.)看起来与C语言中的结构体访问以及java语言汇总的对象访问有点类似,如果点表达式出现在等号  左边,调用该属性名称的setter方法。如果点表达式出现在右边,调用该属性名称的getter方法。
  • OC中点表达式(.)其实就是调用对象的settergetter方法的一种快捷方式,self.myString = @"张三";实际就是[self setmyString:@"张三"];

   最后说一下容易出现的问题的地方,根据我个人的经验,最容易出问题的地方就是对属性xx或成员变量_xx的初始化的地方和调用时机,直接通过例子来看,我们将属性和实例变量的初始化放在重写的get方法中,于是我们在 - (void)viewDidLoad 中使用_invoiceInfoImageView来进行布局时,实际上因为在这之前也没有调用invoiceInfoImageView的get方法,所以此时invoiceInfoImageView的值其实为nil,界面上是空白的。

#import "InvoiceTitleInfoViewController.h"

@interface InvoiceTitleInfoViewController ()

//定义属性invoiceInfoImageView
@property (strong, nonatomic) UIImageView *invoiceInfoImageView;

@end

@implementation InvoiceTitleInfoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"发票抬头";
    
    //用_xx来调用实例变量_invoiceInfoImageView,此时由于没有调用get方法进行初始化,因此此时_invoiceInfoImageView的值为nil
    [self.view addSubview:_invoiceInfoImageView];
    WEAKSELF
    [_invoiceInfoImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(weakSelf.view).mas_offset(0.0f);
        make.left.mas_equalTo(weakSelf.view).mas_offset(15.0f);
        make.right.bottom.mas_equalTo(weakSelf.view).mas_offset(-15.0f);
    }];  
}

//初始化属性invoiceInfoImageView,其实这就是invoiceInfoImageView的get方法
- (UIImageView *)invoiceInfoImageView{
    if (!_invoiceInfoImageView) {
        _invoiceInfoImageView = [[UIImageView alloc] init];
        _invoiceInfoImageView.image = [UIImage imageNamed:@"invoice_title_info"];
    }
    return _invoiceInfoImageView;
}

如果我们在 使用self.xx来调用变量,则会调用invoiceInfoImageView的get方法,进行初始化,界面布局将会显示我们想要的图片。此外,如果我们再使用_xx之前用self.xx调用过变量invoiceInfoImageView,则同样会调用其get方法从而触发invoiceInfoImageView的初始化,这样也不会影响布局。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"发票抬头";
    
    //用self.xx来调用invoiceInfoImageView
    [self.view addSubview:self.invoiceInfoImageView];
    WEAKSELF
    [self.invoiceInfoImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(weakSelf.view).mas_offset(0.0f);
        make.left.mas_equalTo(weakSelf.view).mas_offset(15.0f);
        make.right.bottom.mas_equalTo(weakSelf.view).mas_offset(-15.0f);
    }];  
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"发票抬头";
    
    //先用self.invoiceInfoImageView触发get方法进行初始化,这样_invoiceInfoImageView的值被初始化后不为nil
    self.invoiceInfoImageView;
    [self.view addSubview:_invoiceInfoImageView];
    WEAKSELF
    [_invoiceInfoImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(weakSelf.view).mas_offset(0.0f);
        make.left.mas_equalTo(weakSelf.view).mas_offset(15.0f);
        make.right.bottom.mas_equalTo(weakSelf.view).mas_offset(-15.0f);
    }]; 
}

还有一点值得注意的就是我们前面提到过的,如果我们同时手动重写了一个属性的get和set方法的话,Xcode不会再自动生成带有下划线的私有成员变量了。在我们只定义了get方法时一切都没有问题,但是一旦我们又重写set方法,会发现用到_xx的地方就会报错。

 

 

                                                  转载自:http://www.cnblogs.com/mukekeheart/p/8251366.html

import socket import struct import argparse import random import string import time import threading from collections import defaultdict " python mdns_responder.py --iface_ip 192.168.0.100 " MDNS_ADDR = "224.0.0.251" MDNS_PORT = 5353 class DNSError(Exception): """DNS错误基类""" pass def generate_random_service_id(length=8): """生成8位随机服务ID(大写字母+数字)""" return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length)) def generate_random_hostname_suffix(length=8): """生成随机主机名后缀(数字)""" return ''.join(random.choices(string.digits, k=length)) def parse_dns_name(data: bytes, offset: int) -> tuple: """ 解析DNS名称(支持压缩指针) 返回: (域名, 新偏移量) """ labels = [] traversed = set() while offset < len(data): length = data[offset] offset += 1 if length == 0: # 结束标记 break # 处理压缩指针 if length & 0xC0 == 0xC0: if offset >= len(data): raise DNSError("压缩指针不完整") ptr_offset = ((length & 0x3F) << 8) | data[offset] offset += 1 if ptr_offset in traversed: break traversed.add(ptr_offset) if ptr_offset >= len(data): break name_part, _ = parse_dns_name(data, ptr_offset) labels.append(name_part) break # 处理普通标签 end = offset + length if end > len(data): break labels.append(data[offset:end].decode('utf-8', 'ignore')) offset = end return '.'.join(labels), offset def build_dns_name(name: str, compress_dict: dict, base_offset: int) -> bytes: """ 构建DNS名称(支持压缩) compress_dict: {域名: 偏移量} base_offset: 当前数据起始偏移 """ encoded = b'' parts = name.split('.') for i in range(len(parts)): partial = '.'.join(parts[i:]) # 应用压缩 if len(partial) > 3 and partial in compress_dict: ptr = compress_dict[partial] if ptr < 0x4000: encoded += struct.pack('!H', 0xC000 | ptr) return encoded # 添加标签 label = parts[i] if not 1 <= len(label) <= 63: raise DNSError(f"无效标签长度: {len(label)}") encoded += bytes([len(label)]) + label.encode('utf-8') # 注册压缩点 current = '.'.join(parts[i:]) pos = base_offset + len(encoded) - len(label) - 1 if len(current) > 3 and current not in compress_dict: compress_dict[current] = pos encoded += b'\x00' return encoded def create_service_response(transaction_id: int, service_type: str, instance_name: str, host_name: str, ip: str, port: int) -> bytes: """ 创建单服务响应包 """ compress_dict = {} parts = [] current_offset = 12 # DNS头部长12字节 # === DNS头部 === flags = 0x8400 # QR=1, AA=1 qdcount = 0 # 无查询部分 ancount = 3 # PTR + SRV + A header = struct.pack("!HHHHHH", transaction_id, flags, qdcount, ancount, 0, 0) parts.append(header) # === PTR记录 === ptr_name = build_dns_name(service_type, compress_dict, current_offset) ptr_data = build_dns_name(instance_name, compress_dict, current_offset + len(ptr_name) + 10) ptr_record = ( ptr_name + struct.pack("!HHIH", 12, 1, 4500, len(ptr_data)) + # TYPE=PTR, CLASS=IN, TTL=120 ptr_data ) parts.append(ptr_record) current_offset += len(ptr_record) # === SRV记录 === srv_name = build_dns_name(instance_name, compress_dict, current_offset) srv_data = struct.pack("!HHH", 0, 0, port) # 优先级, 权重, 端口 srv_data += build_dns_name(host_name, compress_dict, current_offset + len(srv_name) + 10 + len(srv_data)) srv_record = ( srv_name + struct.pack("!HHIH", 33, 1, 120, len(srv_data)) + # TYPE=SRV srv_data ) parts.append(srv_record) current_offset += len(srv_record) # === A记录 === a_name = build_dns_name(host_name, compress_dict, current_offset) a_data = socket.inet_aton(ip) a_record = ( a_name + struct.pack("!HHIH", 1, 1, 120, 4) + # TYPE=A a_data ) parts.append(a_record) return b''.join(parts) def create_pair_response(transaction_id: int, airplay_instance: str, raop_instance: str, host_name: str, ip: str) -> bytes: """ 创建服务对响应包(AirPlay + RAOP) """ compress_dict = {} parts = [] current_offset = 12 # === DNS头部 === flags = 0x8400 qdcount = 0 ancount = 5 # 2 PTR + 2 SRV + 1 A header = struct.pack("!HHHHHH", transaction_id, flags, qdcount, ancount, 0, 0) parts.append(header) # === AirPlay PTR === ptr_ap_name = build_dns_name("_airplay._tcp.local", compress_dict, current_offset) ptr_ap_data = build_dns_name(airplay_instance, compress_dict, current_offset + len(ptr_ap_name) + 10) ptr_ap_record = ptr_ap_name + struct.pack("!HHIH", 12, 1, 120, len(ptr_ap_data)) + ptr_ap_data parts.append(ptr_ap_record) current_offset += len(ptr_ap_record) # === AirPlay SRV === srv_ap_name = build_dns_name(airplay_instance, compress_dict, current_offset) srv_ap_data = struct.pack("!HHH", 0, 0, 5000) srv_ap_data += build_dns_name(host_name, compress_dict, current_offset + len(srv_ap_name) + 10 + len(srv_ap_data)) srv_ap_record = srv_ap_name + struct.pack("!HHIH", 33, 1, 120, len(srv_ap_data)) + srv_ap_data parts.append(srv_ap_record) current_offset += len(srv_ap_record) # === RAOP PTR === ptr_raop_name = build_dns_name("_raop._tcp.local", compress_dict, current_offset) ptr_raop_data = build_dns_name(raop_instance, compress_dict, current_offset + len(ptr_raop_name) + 10) ptr_raop_record = ptr_raop_name + struct.pack("!HHIH", 12, 1, 120, len(ptr_raop_data)) + ptr_raop_data parts.append(ptr_raop_record) current_offset += len(ptr_raop_record) # === RAOP SRV === srv_raop_name = build_dns_name(raop_instance, compress_dict, current_offset) srv_raop_data = struct.pack("!HHH", 0, 0, 7000) srv_raop_data += build_dns_name(host_name, compress_dict, current_offset + len(srv_raop_name) + 10 + len(srv_raop_data)) srv_raop_record = srv_raop_name + struct.pack("!HHIH", 33, 1, 120, len(srv_raop_data)) + srv_raop_data parts.append(srv_raop_record) current_offset += len(srv_raop_record) # === 共享A记录 === a_name = build_dns_name(host_name, compress_dict, current_offset) a_data = socket.inet_aton(ip) a_record = a_name + struct.pack("!HHIH", 1, 1, 120, 4) + a_data parts.append(a_record) return b''.join(parts) class MDNSResponder: def __init__(self, iface_ip: str): """mDNS响应器初始化""" self.iface_ip = iface_ip self.services = [] # 所有服务对 self.service_map = defaultdict(list) # 服务类型到实例的映射 self.running = False # 创建socket self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 多播设置 self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(iface_ip)) try: self.sock.bind(("", MDNS_PORT)) mreq = struct.pack("!4s4s", socket.inet_aton(MDNS_ADDR), socket.inet_aton(iface_ip)) self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) print(f"[mDNS] Socket bound to {MDNS_ADDR}:{MDNS_PORT}") except Exception as e: raise RuntimeError(f"Socket初始化失败: {e}") def generate_services(self, start_ip: str, count: int): """生成服务对(每对包含AirPlay和RAOP服务)""" base_ip, start_num = start_ip.rsplit('.', 1) start_num = int(start_num) for i in range(count): # 生成递增IP地址 ip = f"{base_ip}.{start_num + i}" # 生成唯一服务ID(8位字符) service_id = generate_random_service_id() # 生成主机名(主机名后缀为8位数字) host_suffix = generate_random_hostname_suffix() host_name = f"host-{host_suffix}.local" # AirPlay实例名 airplay_instance = f"{service_id}._airplay._tcp.local" # RAOP实例名(遵循Apple规范) raop_instance = f"{service_id}@AirPlay._raop._tcp.local" # 添加到服务列表 self.services.append({ "ip": ip, "host_name": host_name, "airplay_instance": airplay_instance, "raop_instance": raop_instance }) # 添加到服务映射 self.service_map["_airplay._tcp.local"].append( (airplay_instance, host_name, ip, 5000) ) self.service_map["_raop._tcp.local"].append( (raop_instance, host_name, ip, 7000) ) print(f"[生成服务] 已创建 {count} 对服务") def start(self): """启动mDNS响应器""" self.running = True print(f"[mDNS响应器] 在 {self.iface_ip} 上启动") while self.running: try: data, addr = self.sock.recvfrom(2048) if len(data) < 12: continue # 处理数据包 self.handle_packet(data, addr) except Exception as e: if self.running: # 防止关闭时的报错 print(f"处理报文错误: {e}") def handle_packet(self, data: bytes, addr: tuple): """处理接收到的mDNS查询(支持多问题查询)""" # 解析头部 transaction_id = struct.unpack("!H", data[0:2])[0] flags = struct.unpack("!H", data[2:4])[0] qdcount = struct.unpack("!H", data[4:6])[0] # 问题数量 # 只处理查询请求 if flags & 0x8000: return offset = 12 # DNS头部后开始解析问题 services_to_respond = set() # 收集需要响应的服务类型 # 循环读取所有问题 for _ in range(qdcount): try: # 解析域名 qname, new_offset = parse_dns_name(data, offset) # 读取类型和类(各2字节) if new_offset + 4 > len(data): break qtype, qclass = struct.unpack("!HH", data[new_offset:new_offset+4]) offset = new_offset + 4 # 只处理PTR查询(qtype=12)且类为IN(qclass=1) if qtype != 12 or qclass != 1: continue service_type = qname.rstrip('.').lower() if service_type in self.service_map: services_to_respond.add(service_type) except DNSError as e: # 跳过当前问题,继续下一个 continue # 对每个需要响应的服务类型,发送响应 for service_type in services_to_respond: print(f"[查询] 来自 {addr[0]} 查询 {service_type}") for instance_info in self.service_map[service_type]: instance_name, host_name, ip, port = instance_info try: response = create_service_response( transaction_id, service_type, instance_name, host_name, ip, port ) self.sock.sendto(response, (MDNS_ADDR, MDNS_PORT)) except Exception as e: print(f"构建响应错误: {e}") def stop(self): """停止响应器""" self.running = False self.sock.close() print("[mDNS响应器] 已停止") class ServiceAnnouncer: def __init__(self, iface_ip: str, services: list): """服务广播器初始化""" self.iface_ip = iface_ip self.services = services self.running = False # 创建广播socket self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255) self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(iface_ip)) def start(self): """启动服务广播""" self.running = True print("[广播器] 开始周期性广播服务") broadcast_count = 0 while self.running: try: broadcast_count += 1 broadcast_time = time.strftime("%Y-%m-%d %H:%M:%S") print(f"\n[广播 #{broadcast_count}] 开始于 {broadcast_time}") # 广播每个服务对 for idx, service in enumerate(self.services): transaction_id = random.randint(0, 0xFFFF) try: response = create_pair_response( transaction_id, service["airplay_instance"], service["raop_instance"], service["host_name"], service["ip"] ) self.sock.sendto(response, (MDNS_ADDR, MDNS_PORT)) print(f" 已广播服务对 #{idx+1}/{len(self.services)}: {service['ip']}") except Exception as e: print(f" 广播错误: {e}") # 等待下次广播 sleep_interval = 120 # 2分钟 for _ in range(sleep_interval): if not self.running: break time.sleep(1) except Exception as e: print(f"广播错误: {e}") def stop(self): """停止广播""" self.running = False self.sock.close() print("[广播器] 已停止") def main(): parser = argparse.ArgumentParser(description="AirPlay & RAOP mDNS 批量响应器") parser.add_argument("--iface_ip", required=True, help="网络接口IP") parser.add_argument("--start_ip", default="192.168.0.2", help="起始IP地址 (默认: 192.168.0.2)") parser.add_argument("--count", type=int, default=100, help="服务对数量 (默认: 100)") args = parser.parse_args() # 创建响应器 responder = MDNSResponder(args.iface_ip) # 生成服务 responder.generate_services(args.start_ip, args.count) # 启动响应线程 responder_thread = threading.Thread(target=responder.start, daemon=True) responder_thread.start() print("服务已启动,按Ctrl+C退出...") try: # 主线程等待中断信号 while True: time.sleep(1) except KeyboardInterrupt: print("正在停止...") # 设置停止标志(如果类中有) responder.running = False # 也可以调用stop方法,但注意线程可能阻塞在recvfrom或sleep中 # 所以设置标志后,还需要中断这些阻塞(例如关闭socket) responder.stop() # 等待线程结束(如果有必要) responder_thread.join(timeout=1.0) print("已退出") if __name__ == "__main__": main()
10-25
这里是m文件 应该是这里面具体写了吧 #import "TPBCommonTableController.h" #import "TPBTableTextSectionHeader.h" #import "TPBTableTextSectionFooter.h" #import "TPBDesignKit.h" #import "UIView+TPBExpanded.h" #import "MASConstraint+TPBExpanded.h" #import "TPBTableSectionModel.h" #import "TPBCommonInputView.h" #import "TPBCustomTableCellBaseModel.h" #import "TPBCustomViewTableCell.h" #import "TPBTitleSubtitleTableCell.h" #import "TPBSwitchTableCell.h" #import "TPBListButtonTableCell.h" #import "TPBProgressTableCell.h" #import "TPBCommonInputTableCell.h" #import "TPBCheckTableCell.h" #import "TPBJumpSelectTableCell.h" #import "TPBMenuSelectTableCell.h" #import "TPBListInputTableCell.h" #import "TPBTitleActionTableCell.h" @interface TPBCommonTableController () < TPBTableCommonInputDelegate, TPBTableListInputDelegate > @property (nonatomic, assign) UITableViewStyle tableViewStyle; @property (nonatomic, strong) UIView *tpbSearchBarPlaceholderView; @property (nonatomic, strong) TPBTableSearchHeader *tpbInnerSearchBar; @property (nonatomic, strong) TPBEmptyView *emptyView; @property (nonatomic, strong) TPKeyboardAvoidingTableView *tableView; @property (nonatomic, strong) MASConstraint *emptyCenterYConstraint; @property (nonatomic, strong) MASConstraint *searchBarTopConstraint; @property (nonatomic, strong) MASConstraint *searchBarHeightConstraint; @end @implementation TPBCommonTableController - (instancetype)init { if (@available(iOS 13.0, *)) { return [self initWithTableViewStyle:UITableViewStyleInsetGrouped]; } else { return [self initWithTableViewStyle:UITableViewStyleGrouped]; } } - (instancetype)initWithTableViewStyle:(UITableViewStyle)style { if (self = [super initWithNibName:nil bundle:nil]) { _tableViewStyle = style; _hideCellSeparator = NO; } return self; } - (instancetype)initWithCoder:(NSCoder *)coder { if (self = [super initWithCoder:coder]) { UITableViewStyle style = UITableViewStyleGrouped; if (@available(iOS 13.0, *)) { style = UITableViewStyleInsetGrouped; } _tableViewStyle = style; _hideCellSeparator = NO; } return self; } - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { UITableViewStyle style = UITableViewStyleGrouped; if (@available(iOS 13.0, *)) { style = UITableViewStyleInsetGrouped; } _tableViewStyle = style; _hideCellSeparator = NO; } return self; } - (void)dealloc { [self.view tpbRemoveForKeyboardEvent]; } - (void)tpbSetupInitialData { [super tpbSetupInitialData]; self.keyboardBehavior = TPBCommonTableKeyboardBehaviorDismissOnly; } - (void)tpbSetupSubviews { [super tpbSetupSubviews]; [self.tableView insertSubview:self.emptyView atIndex:0]; [self.view addSubview:self.tableView]; [self.view addSubview:self.tpbInnerSearchBar]; } - (void)tpbMakeConstraint { [super tpbMakeConstraint]; [self.emptyView mas_remakeConstraints:^(MASConstraintMaker *make) { make.leading.trailing.equalTo(self.mas_tpSafeAreaLayoutGuide); self.emptyCenterYConstraint = make.centerY.equalTo(self.mas_tpSafeAreaLayoutGuide); }]; [self.tableView mas_remakeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.mas_tpSafeAreaLayoutGuide); make.leading.trailing.equalTo(self.mas_tpSafeAreaLayoutGuide); make.bottom.equalTo(self.view); }]; CGFloat searchHeight = [self.tpbInnerSearchBar preferredHeight]; [self.tpbInnerSearchBar mas_remakeConstraints:^(MASConstraintMaker *make) { self.searchBarTopConstraint = make.top.equalTo(self.mas_tpSafeAreaLayoutGuide).offset(0); make.leading.trailing.equalTo(self.mas_tpSafeAreaLayoutGuide); self.searchBarHeightConstraint = make.height.equalTo(@(searchHeight)); }]; } - (void)tpbBindActions { [super tpbBindActions]; TPBWeakSelf [self tpbAddContentSizeDidChangeConfig:^(id _Nonnull object, TPBDynamicContentManager * _Nonnull manager) { TPBStrongSelf dispatch_async(dispatch_get_main_queue(), ^{ [_self updateTableHeader]; [_self.tableView reloadData]; }); } notifyWhenRegister:NO]; } - (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; [self.view bringSubviewToFront:self.tpbInnerSearchBar]; [self updateSearchBarPosition]; } #pragma mark - Public - (void)setSectionArray:(NSArray<TPBTableSectionModel *> *)sectionArray { _sectionArray = sectionArray; [self.tableView reloadData]; } - (void)setSearchBarShow:(BOOL)searchBarShow { _searchBarShow = searchBarShow; self.tpbInnerSearchBar.hidden = !searchBarShow; [self updateTableHeader]; } - (NSString *)searchPlaceholder { return self.tpbInnerSearchBar.searchPlaceholder; } - (void)setSearchPlaceholder:(NSString *)searchPlaceholder { self.tpbInnerSearchBar.searchPlaceholder = searchPlaceholder; } - (void)updateSearchKey:(NSString *)searchKey { [self.tpbInnerSearchBar updateSearchKey:searchKey]; } - (void)setCustomTableHeaderView:(UIView *)customTableHeaderView { _customTableHeaderView = customTableHeaderView; [self updateTableHeader]; } - (void)setKeyboardBehavior:(TPBCommonTableKeyboardBehavior)keyboardBehavior { _keyboardBehavior = keyboardBehavior; switch (keyboardBehavior) { case TPBCommonTableKeyboardBehaviorDismissOnly: [self.view tpbRegisterForKeyboardEvent]; break; case TPBCommonTableKeyboardBehaviorDismissAndRespondClick: [self.view tpbRemoveForKeyboardEvent]; break; default: break; } } - (void)focusToFirstEligibleElement { NSIndexPath *indexPath = nil; for (NSUInteger section = 0; section < self.sectionArray.count; section++) { TPBTableSectionModel *sectionModel = self.sectionArray[section]; for (NSUInteger row = 0; row < sectionModel.cellModelArray.count; row++) { TPBBaseTableCellModel *cellModel = sectionModel.cellModelArray[row]; if (cellModel.isAutoFocusEnabled) { indexPath = [NSIndexPath indexPathForRow:row inSection:section]; break; } } if (indexPath != nil) break; } if (indexPath == nil) return; UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; if (cell == nil) return; if ([TPBA11yHelper isVoiceOverOn]) { UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, cell); } else { [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES]; } } #pragma mark - UITableViewDataSource & UITableViewDelegate // 注册自定义Cell - (void)registerCustomCellWithCellModel:(TPBCustomTableCellBaseModel *)customCellModel tableView:(UITableView *)tableView cellIdentifier:(NSString *)cellIdentifier { if (customCellModel.cellClass != nil) { BOOL isCellClassValid = [customCellModel.cellClass isSubclassOfClass:[UITableViewCell class]]; BOOL isValid = isCellClassValid; NSAssert(isCellClassValid, @"TPBCustomBaseTableCellModel's cellClass「%@」is not Subclass of UITableViewCell!", NSStringFromClass(customCellModel.cellClass)); if (!isValid) { return; } // 注册Cell Class [tableView registerClass:customCellModel.cellClass forCellReuseIdentifier:cellIdentifier]; } else if (customCellModel.cellNib != nil) { // 注册Cell Nib [tableView registerNib:customCellModel.cellNib forCellReuseIdentifier:cellIdentifier]; } } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.sectionArray.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return 0; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; return sectionModel.cellModelArray.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section < 0 || self.sectionArray.count <= indexPath.section) { return [[UITableViewCell alloc] init]; } TPBTableSectionModel *sectionModel = self.sectionArray[indexPath.section]; if (indexPath.row < 0 || sectionModel.cellModelArray.count <= indexPath.row) { return [[UITableViewCell alloc] init]; } BOOL isRTL = self.view.effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; BOOL isLast = indexPath.row == sectionModel.cellModelArray.count - 1; TPBBaseTableCellModel *model = sectionModel.cellModelArray[indexPath.row]; switch (model.cellType) { case TPBTableCellTypeCustomCell: { if ([model isKindOfClass:[TPBCustomTableCellBaseModel class]]) { TPBCustomTableCellBaseModel *cellModel = (TPBCustomTableCellBaseModel *)model; NSString *cellIdentifier = [cellModel effectiveCellIdentifier:isRTL]; UITableViewCell *cell; if (TPBA11yHelper.isVoiceOverOn && cellModel.cellClass != nil) { cell = [[cellModel.cellClass alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } if (cell == nil) { // 若无法获取Cell,说明可能是未注册自定义Cell,则注册自定义Cell之后尝试重新取值 [self registerCustomCellWithCellModel:cellModel tableView:tableView cellIdentifier:cellIdentifier]; cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } if (cellModel.cellConfigCallback) { cellModel.cellConfigCallback(tableView, indexPath, cell, cellModel); } if ([cell isKindOfClass:[TPBBaseTableViewCell class]]) { TPBBaseTableViewCell *baseCell = (TPBBaseTableViewCell *)cell; [baseCell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; } return cell; } } break; case TPBTableCellTypeCustomView: { if ([model isKindOfClass:[TPBCustomViewTableCellModel class]]) { TPBCustomViewTableCellModel *cellModel = (TPBCustomViewTableCellModel *)model; NSString *cellIdentifier = [TPBCustomViewTableCell cellIdentifier:isRTL]; TPBCustomViewTableCell *cell; cell = [[TPBCustomViewTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeTitleSubtitle: { if ([model isKindOfClass:[TPBTitleSubtitleTableCellModel class]]) { TPBTitleSubtitleTableCellModel *cellModel = (TPBTitleSubtitleTableCellModel *)model; NSString *cellIdentifier = [TPBTitleSubtitleTableCell cellIdentifier:isRTL]; TPBTitleSubtitleTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBTitleSubtitleTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeSwitch: { if ([model isKindOfClass:[TPBSwitchTableCellModel class]]) { TPBSwitchTableCellModel *cellModel = (TPBSwitchTableCellModel *)model; NSString *cellIdentifier = [TPBSwitchTableCell cellIdentifier:isRTL]; TPBSwitchTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBSwitchTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeListButton: { if ([model isKindOfClass:[TPBListButtonTableCellModel class]]) { TPBListButtonTableCellModel *cellModel = (TPBListButtonTableCellModel *)model; NSString *cellIdentifier = [TPBListButtonTableCell cellIdentifier:isRTL]; TPBListButtonTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBListButtonTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeProgress: { if ([model isKindOfClass:[TPBProgressTableCellModel class]]) { TPBProgressTableCellModel *cellModel = (TPBProgressTableCellModel *)model; NSString *cellIdentifier = [TPBProgressTableCell cellIdentifier:isRTL]; TPBProgressTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBProgressTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeCommonInput: { if ([model isKindOfClass:[TPBCommonInputTableCellModel class]]) { TPBCommonInputTableCellModel *cellModel = (TPBCommonInputTableCellModel *)model; NSString *cellIdentifier = [TPBCommonInputTableCell cellIdentifier:isRTL]; TPBCommonInputTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBCommonInputTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } cell.inputDelegate = self; [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeCheck: { if ([model isKindOfClass:[TPBCheckTableCellModel class]]) { TPBCheckTableCellModel *cellModel = (TPBCheckTableCellModel *)model; BOOL isCustomizedCellHeight = cellModel.height.isCustomHeight; NSString *cellIdentifier = [TPBCheckTableCell cellIdentifier:isRTL]; TPBCheckTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBCheckTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel isCustomizedCellHeight:isCustomizedCellHeight]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeJumpSelect: { if ([model isKindOfClass:[TPBJumpSelectTableCellModel class]]) { TPBJumpSelectTableCellModel *cellModel = (TPBJumpSelectTableCellModel *)model; NSString *cellIdentifier = [TPBJumpSelectTableCell cellIdentifier:isRTL]; TPBJumpSelectTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBJumpSelectTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && TPBIsEmptyString(cellModel.title) && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeMenuSelect: { if ([model isKindOfClass:[TPBMenuSelectTableCellModel class]]) { TPBMenuSelectTableCellModel *cellModel = (TPBMenuSelectTableCellModel *)model; NSString *cellIdentifier = [TPBMenuSelectTableCell cellIdentifier:isRTL]; TPBMenuSelectTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBMenuSelectTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeListInput: { if ([model isKindOfClass:[TPBListInputTableCellModel class]]) { TPBListInputTableCellModel *cellModel = (TPBListInputTableCellModel *)model; NSString *cellIdentifier = [TPBListInputTableCell cellIdentifier:isRTL]; TPBListInputTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBListInputTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } cell.inputDelegate = self; [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; case TPBTableCellTypeTitleAction: { if ([model isKindOfClass:[TPBTitleActionTableCellModel class]]) { TPBTitleActionTableCellModel *cellModel = (TPBTitleActionTableCellModel *)model; NSString *cellIdentifier = [TPBTitleActionTableCell cellIdentifier:isRTL]; TPBTitleActionTableCell *cell; if (TPBA11yHelper.isVoiceOverOn) { cell = [[TPBTitleActionTableCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]; } if (cell == nil) { cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; } [cell updateCellModel:cellModel]; [cell updateBottomSeparatorShow:!self.hideCellSeparator && !isLast && !cellModel.hideCellSeparator]; return cell; } } break; } return [[UITableViewCell alloc] init]; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section < 0 || self.sectionArray.count <= indexPath.section) { return UITableViewAutomaticDimension; } TPBTableSectionModel *sectionModel = self.sectionArray[indexPath.section]; if (indexPath.row < 0 || sectionModel.cellModelArray.count <= indexPath.row) { return UITableViewAutomaticDimension; } TPBBaseTableCellModel *cellModel = sectionModel.cellModelArray[indexPath.row]; if (cellModel.height.isCustomHeight) { return cellModel.height.customHeight; } return UITableViewAutomaticDimension; } // Header - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return [UIView new]; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; if (sectionModel.customHeaderView != nil) { return sectionModel.customHeaderView; } if (TPBIsEmptyString(sectionModel.headerTitle) && sectionModel.headerAction == nil) { return [UIView new]; } TPBTableTextSectionHeader *headerView = [TPBTableTextSectionHeader new]; [headerView updateSectionModel:sectionModel]; return headerView; } - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return TPBDesign.list.sectionHeaderHeight; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; if (sectionModel.sectionHeaderHeight.isCustomHeight) { return sectionModel.sectionHeaderHeight.customHeight; } if (sectionModel.customHeaderView != nil) { return UITableViewAutomaticDimension; } if (TPBIsEmptyString(sectionModel.headerTitle) && sectionModel.headerAction == nil) { if (self.searchBarShow && section == 0) { return 10; } return TPBDesign.list.sectionHeaderHeight; } return UITableViewAutomaticDimension; } // Footer - (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return [UIView new]; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; if (sectionModel.customFooterView != nil) { return sectionModel.customFooterView; } if (TPBIsEmptyString(sectionModel.footerTitle) && sectionModel.footerAction == nil) { return [UIView new]; } TPBTableTextSectionFooter *footerView = [TPBTableTextSectionFooter new]; [footerView updateSectionModel:sectionModel]; return footerView; } - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section { if (section < 0 || self.sectionArray.count <= section) { return TPBDesign.list.sectionFooterHeight; } TPBTableSectionModel *sectionModel = self.sectionArray[section]; if (sectionModel.sectionFooterHeight.isCustomHeight) { return sectionModel.sectionFooterHeight.customHeight; } if (sectionModel.customFooterView != nil) { return UITableViewAutomaticDimension; } if (TPBIsEmptyString(sectionModel.footerTitle) && sectionModel.footerAction == nil) { return TPBDesign.list.sectionFooterHeight; } return UITableViewAutomaticDimension; } // TableViewCell点击 - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:NO]; if (indexPath.section < 0 || self.sectionArray.count <= indexPath.section) { return NO; } TPBTableSectionModel *sectionModel = self.sectionArray[indexPath.section]; if (indexPath.row < 0 || sectionModel.cellModelArray.count <= indexPath.row) { return NO; } TPBBaseTableCellModel *cellModel = sectionModel.cellModelArray[indexPath.row]; switch (cellModel.cellType) { case TPBTableCellTypeCustomCell: case TPBTableCellTypeCustomView: case TPBTableCellTypeTitleSubtitle: case TPBTableCellTypeSwitch: case TPBTableCellTypeCheck: case TPBTableCellTypeListButton: case TPBTableCellTypeJumpSelect: case TPBTableCellTypeMenuSelect: case TPBTableCellTypeProgress: case TPBTableCellTypeTitleAction: return YES; case TPBTableCellTypeCommonInput: case TPBTableCellTypeListInput: return NO; } } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:NO]; if (indexPath.section < 0 || self.sectionArray.count <= indexPath.section) { return; } TPBTableSectionModel *sectionModel = self.sectionArray[indexPath.section]; if (indexPath.row < 0 || sectionModel.cellModelArray.count <= indexPath.row) { return; } if (self.keyboardBehavior == TPBCommonTableKeyboardBehaviorDismissAndRespondClick) { [self tpbHideKeyboard]; } TPBBaseTableCellModel *model = sectionModel.cellModelArray[indexPath.row]; switch (model.cellType) { case TPBTableCellTypeCustomCell: { if ([model isKindOfClass:[TPBCustomTableCellBaseModel class]]) { TPBCustomTableCellBaseModel *cellModel = (TPBCustomTableCellBaseModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeCustomView: { if ([model isKindOfClass:[TPBCustomViewTableCellModel class]]) { TPBCustomViewTableCellModel *cellModel = (TPBCustomViewTableCellModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeTitleSubtitle: { if ([model isKindOfClass:[TPBTitleSubtitleTableCellModel class]]) { TPBTitleSubtitleTableCellModel *cellModel = (TPBTitleSubtitleTableCellModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeSwitch: { if ([model isKindOfClass:[TPBSwitchTableCellModel class]]) { TPBSwitchTableCellModel *cellModel = (TPBSwitchTableCellModel *)model; if (cellModel.switchDidClickHotZoneCallback) { cellModel.switchDidClickHotZoneCallback(); } } } break; case TPBTableCellTypeListButton: { if ([model isKindOfClass:[TPBListButtonTableCellModel class]]) { TPBListButtonTableCellModel *cellModel = (TPBListButtonTableCellModel *)model; if (cellModel.actionEnabled && cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeCheck: { if ([model isKindOfClass:[TPBCheckTableCellModel class]]) { TPBCheckTableCellModel *cellModel = (TPBCheckTableCellModel *)model; if (cellModel.checkEnabled && cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeJumpSelect: { if ([model isKindOfClass:[TPBJumpSelectTableCellModel class]]) { TPBJumpSelectTableCellModel *cellModel = (TPBJumpSelectTableCellModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } case TPBTableCellTypeTitleAction: { if ([model isKindOfClass:[TPBTitleActionTableCellModel class]]) { TPBTitleActionTableCellModel *cellModel = (TPBTitleActionTableCellModel *)model; if (cellModel.didSelectCellCallback) { cellModel.didSelectCellCallback(cellModel, indexPath); } } } break; case TPBTableCellTypeMenuSelect: break; case TPBTableCellTypeProgress: case TPBTableCellTypeCommonInput: case TPBTableCellTypeListInput: break; } } //- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath{ // if (indexPath.row < 0 || indexPath.row >= self.cloudDeviceList.count) { // return NO; // } // TPBDMECDevice *ecDevice = self.cloudDeviceList[indexPath.row]; // return [self canForgetWithDevice:ecDevice]; //} //- (NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath { // if (indexPath.row < 0 || indexPath.row >= self.cloudDeviceList.count) { // return @[]; // } // TPBDMECDevice *ecDevice = self.cloudDeviceList[indexPath.row]; // TPBWeakSelf; // UITableViewRowAction *deleteAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDestructive title:gControllerCloudAccess.controllerCloudAccessForget handler:^(UITableViewRowAction *action, NSIndexPath *indexPath){ // TPBStrongSelf; // [_self popAlertControlForUnbindActionWithECDevice:ecDevice]; // }]; // deleteAction.backgroundColor = [UIColor tpbRed]; // return @[deleteAction]; //} #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateSearchBarPosition]; } #pragma mark - TPBTableSearchHeaderDelegate - (void)tpbTableSearchHeaderHideKeyboard { [self tpbHideKeyboard]; } #pragma mark - TPBTableCommonInputDelegate - (void)tableCommonInputFieldDidClickReturn:(TPBCommonInputView *)inputView textField:(UITextField *)textField { [self handleUserClickReturnForTextField:textField]; } #pragma mark - TPBTableListInputDelegate - (void)tableListInputFieldDidClickReturn:(TPBListInputTableCell *)inputCell textField:(UITextField *)textField { [self handleUserClickReturnForTextField:textField]; } #pragma mark - Private - (void)updateSearchBarPosition { CGFloat tableY = self.tableView.frame.origin.y; CGRect convertedRect = [self.tpbSearchBarPlaceholderView convertRect:self.tpbSearchBarPlaceholderView.bounds toView:self.view]; CGFloat diffY = convertedRect.origin.y - tableY; CGFloat targetConstant = MAX(0, diffY); if (self.searchBarTopConstraint.tpbConstant != targetConstant) { self.searchBarTopConstraint.tpbConstant = targetConstant; } } // 用户点击UITextField键盘Return - (void)handleUserClickReturnForTextField:(UITextField *)textField { if (textField.returnKeyType != UIReturnKeyNext) { [textField resignFirstResponder]; return; } if ([self.tableView focusNextTextField]) { } else { [textField resignFirstResponder]; } } - (void)updateTableHeader { if (self.searchBarShow) { CGFloat searchBarHeight = [self.tpbInnerSearchBar preferredHeight]; self.tpbSearchBarPlaceholderView.frame = CGRectMake(0, 0, self.view.bounds.size.width, searchBarHeight); self.tableView.tableHeaderView = self.tpbSearchBarPlaceholderView; self.searchBarHeightConstraint.tpbConstant = searchBarHeight; } else if (self.customTableHeaderView) { self.tableView.tableHeaderView = self.customTableHeaderView; } else { UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, CGFLOAT_MIN)]; self.tableView.tableHeaderView = view; } } #pragma mark - Property - (TPBEmptyView *)emptyView { if (!_emptyView) { _emptyView = [TPBEmptyView new]; _emptyView.hidden = YES; } return _emptyView; } - (UIView *)tpbSearchBarPlaceholderView { if (!_tpbSearchBarPlaceholderView) { _tpbSearchBarPlaceholderView = [UIView new]; } return _tpbSearchBarPlaceholderView; } - (TPBTableSearchHeader *)tpbInnerSearchBar { if (!_tpbInnerSearchBar) { CGRect frame = CGRectMake(0, 0, self.view.bounds.size.width, 62); _tpbInnerSearchBar = [[TPBTableSearchHeader alloc] initWithFrame:frame]; _tpbInnerSearchBar.delegate = self; _tpbInnerSearchBar.hidden = YES; _tpbInnerSearchBar.backgroundColor = [UIColor tpbBackground]; } return _tpbInnerSearchBar; } - (UITableView *)tableView { if (!_tableView) { _tableView = [[TPKeyboardAvoidingTableView alloc] initWithFrame:CGRectZero style:self.tableViewStyle]; if (@available(iOS 13.0, *)) { _tableView.automaticallyAdjustsScrollIndicatorInsets = YES; } if (@available(iOS 15.0, *)) { UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, CGFLOAT_MIN)]; _tableView.tableHeaderView = view; _tableView.sectionHeaderTopPadding = 0; } _tableView.backgroundColor = [UIColor clearColor]; _tableView.estimatedRowHeight = 72; _tableView.estimatedSectionHeaderHeight = 68; _tableView.showsHorizontalScrollIndicator = NO; _tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag; _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; _tableView.sectionIndexColor = [UIColor tpbTableSectionIndexColor]; _tableView.delegate = self; _tableView.dataSource = self; NSArray<Class> *cellClassArray = @[ [TPBCustomViewTableCell class], [TPBTitleSubtitleTableCell class], [TPBSwitchTableCell class], [TPBProgressTableCell class], [TPBCommonInputTableCell class], [TPBCheckTableCell class], [TPBJumpSelectTableCell class], [TPBMenuSelectTableCell class], [TPBListInputTableCell class], [TPBListButtonTableCell class], [TPBTitleActionTableCell class] ]; for (Class cls in cellClassArray) { if ([cls isSubclassOfClass:[TPBBaseTableViewCell class]]) { NSString *cellIdentifier = [cls cellIdentifier]; [_tableView registerClass:cls forCellReuseIdentifier:cellIdentifier]; NSString *rtlCellIdentifier = [cls cellIdentifier:YES]; [_tableView registerClass:cls forCellReuseIdentifier:rtlCellIdentifier]; NSString *ltrCellIdentifier = [cls cellIdentifier:NO]; [_tableView registerClass:cls forCellReuseIdentifier:ltrCellIdentifier]; } } } return _tableView; } @end
最新发布
11-29
import queue import threading import xml.etree.ElementTree as ET import os from concurrent.futures.thread import ThreadPoolExecutor from openpyxl import Workbook import re from pathlib import Path from openpyxl.styles.builtins import title from collections import defaultdict import copy suffixes_android = ['strings.xml', 'strings_v6.xml', 'strings_v2.xml'] suffixes_ios = ['Localizable.strings', 'LocalizableAdded.strings'] title_path = set() queues_name_gloable = ['remediate_list', 'all_differ_key_value'] queues_name = {name: queue.Queue() for name in queues_name_gloable} ios_self_common = [] ios_self_tai = [] lock = threading.Lock() class xmlObj: def __init__(self, key, value, old_value, old_key, path, ios_key, ios_value, ios_path, languages): self.key = key self.xml_path = path self.value = value self.languages = languages self.ios_key = ios_key self.ios_value = ios_value self.ios_path = ios_path self.old_value = old_value self.old_key = old_key def __eq__(self, other): if isinstance(other, xmlObj): return (self.key == other.key and self.xml_path == other.xml_path and self.value == other.value and self.languages == other.languages and self.ios_key == other.ios_key and self.ios_value == other.ios_value and self.ios_path == other.ios_path and self.old_value == other.old_value and self.old_key == other.old_key) return False class iosObj: def __init__(self, key, value, path, androidKey, androidValue, androidPath, languages): self.key = key self.ios_path = path self.value = value self.androidKey = androidKey self.androidValue = androidValue self.androidPath = androidPath self.languages = languages def merge_xml_files(xml_files): try: title_path.add(xml_files.split("\\")[5]) tree = ET.parse(xml_files) root = tree.getroot() language_code = get_android_language(xml_files) android_list = [] for child in root.findall("string"): key = child.attrib.get("name") value = child.text android_list.append(xmlObj(key, value, '', '', xml_files, '', '', '', language_code)) return get_android_ios_obj(android_list) except ET.ParseError as e: print(f"Error parsing file {xml_files}: {e}") def get_android_language(xml_files): switch_dice = { "zh-rMO": "TW", "zh-rZH": "zh-rCN", "pt-rBR": "pt", "es-rLA": "es", "el-rGR": "el", "es-rES": "es" } direct_name = os.path.dirname(xml_files) # 获取目录路径 -> 'res/values-pt-rBR' # 使用正则表达式提取语言标识(如 'pt-rBR') match = re.search(r'values-([a-z]{2}(-[a-zA-Z0-9]{1,8})?)', direct_name) if match: language_code = match.group(1) # 输出: pt-rBR count = language_code.count('-') if count > 0: language_code = switch_dice.get(language_code, language_code) else: language_code = "en" # 默认值(如通用资源目录 values/) return language_code def get_ios_language(file_path): direct_name = os.path.dirname(file_path) language = os.path.basename(direct_name) count = language.count('-') if count == 0: language = language.split('.')[0] elif count == 1: language = language.split('.')[0].split('-')[0] elif count == 2: language = language.split('.')[0].split('-')[2] if language == "zh": language = "zh-rCN" elif language == "Base": language = "en" elif language == "CN": language = "TW" return language def parse_strings_files(ios_files): language = get_ios_language(ios_files) pattern = re.compile(r'"([^"]+)"\s*=\s*"([^"]*)";') with open(ios_files, 'r', encoding='utf-8', errors='ignore') as f: for line in f: match = pattern.match(line) if match: key = match.group(1) value = match.group(2) key = key.replace('\\', '"') value = value.replace('\\', '"') obj = iosObj(key, value, ios_files, '', '', '', language) if "Resource" in ios_files: ios_self_common.append(obj) else: ios_self_tai.append(obj) def travel_root(root_path): android_list = [] for root, dirs, files in os.walk(root_path, followlinks=False): path_android = [os.path.join(root, file) for file in files if file.endswith(tuple(suffixes_android))] [print(f"{file}") or parse_strings_files(os.path.join(root, file)) for file in files if file.endswith(tuple(suffixes_ios))] with ThreadPoolExecutor(max_workers=4) as executor: android_list = list(executor.map(merge_xml_files, path_android)) return android_list def get_android_ios_obj(android_list): queue_names = ['android_self_common', 'android_self_tai', 'android_list_self_common', 'android_list_self_tai', 'merge_xml_obj'] queues = {name: queue.Queue() for name in queue_names} for items_android in android_list: if "main" in items_android.xml_path: queues['android_self_common'].put(items_android) else: queues['android_self_tai'].put(items_android) if not queues['android_self_common'].empty(): queues['android_list_self_common'].put( compare_android_ios(list(queues['android_self_common'].queue), ios_self_common)) queues['android_list_self_tai'].put(compare_android_ios(list(queues['android_self_tai'].queue), ios_self_tai)) queues['merge_xml_obj'] = merge_list(queues['android_self_common'], queues['android_self_tai']) return queues['merge_xml_obj'] def merge_list(android_list_self_common, android_list_self_tai): with lock: merge_xml_obj = [] for _in in range(android_list_self_common.qsize()): merge_xml_obj.append(android_list_self_common.get()) for _in in range(android_list_self_tai.qsize()): merge_xml_obj.append(android_list_self_common.get()) return merge_xml_obj def is_same_language(android_value, ios_value): if android_value.languages == ios_value.languages: return True else: return False def get_all_differ(android_copy, ios_self): seen_keys = set() # 用于去重,确保每个 Android 仅添加一次 for self_ios in ios_self: for self_android in android_copy: for item in self_android: if self_ios.key != item.key and is_same_language(item, self_ios) and self_ios.value != item.value: key_and_value = str(item.key) + str(item.value) # 添加唯一标识避免重复 if key_and_value not in seen_keys: seen_keys.add(key_and_value) xml = xmlObj(item.key, item.value, item.value, item.key, item.xml_path, self_ios.key, self_ios.value, self_ios.ios_path, '') queues_name['all_differ_key_value'].put(xml) def compare_android_ios(android_self, ios_self): print("正在比较......") get_differ_key(android_self, ios_self) android_copy = copy.deepcopy(android_self) android = get_differ_value(android_copy, ios_self) return android def get_differ_key(android_self, ios_self): for self_android in android_self: for self_ios in ios_self: if self_android.value == self_ios.value and is_same_language(self_android, self_ios): if self_android.key != self_ios.key: old_key = self_android.key self_android.key = self_ios.key xml = xmlObj(self_android.key, self_android.value, self_android.value, old_key, self_android.xml_path, self_ios.key, self_ios.value, self_ios.ios_path, '') queues_name['remediate_list'].put(xml) def get_differ_value(android_copy, ios_self): for self_android in android_copy: for self_ios in ios_self: if self_android.key == self_ios.key and is_same_language(self_android, self_ios): if self_ios.value != self_android.value: self_android.value = self_ios.value xml = xmlObj(self_android.key, self_android.value, self_android.old_value, self_android.old_key, self_android.xml_path, self_ios.key, self_ios.value, self_ios.ios_path, '') queues_name['remediate_list'].put(xml) return copy.deepcopy(android_copy) def write_dict_to_xml(data_dict): print("写入xml") group_data = defaultdict(list) for obj in data_dict: for item in obj: group_data[item.xml_path].append(obj) for path, items in group_data.items(): resource = ET.Element('resources') for item in items: et = ET.SubElement( resource, "string", {"name": item.key} ) et.text = item.value tree = ET.ElementTree(resource) ET.indent(tree, space=" ") # 使 XML 格式整齐 tree.write(path, encoding="utf-8", xml_declaration=True, method='xml') print("写入文件") def write_to_excel(android_list_self, is_same): wb = Workbook() for item in list(android_list_self.queue): title = get_parent_directory(item.xml_path) for elem in title_path: if elem in item.xml_path: title = elem # 根据 title 获取或创建工作表 if title in wb.sheetnames: ws = wb[title] else: ws = wb.create_sheet(title=title) ws.append(["key", "value", "ios_key", "ios_value", "ios_path", "android_path", "old_value", "old_key"]) # 将数据追加到对应工作表 ws.append([item.key, item.value, item.ios_key, item.ios_value, item.ios_path, item.xml_path, item.old_value, item.old_key]) desktop_path = os.path.expanduser(r"D:\compare_excel") new_dir = os.path.join(desktop_path, 'android_save') os.makedirs(new_dir, exist_ok=True) if is_same: file_path = os.path.join(desktop_path, 'android_same.xlsx') else: file_path = os.path.join(desktop_path, 'android_not_same.xlsx') file_path = os.path.join(new_dir, file_path) wb.save(file_path) def get_parent_directory(xml_path): path = Path(xml_path) parts = path.parts for i in range(len(parts) - 1): if parts[i].lower() == 'res': return path.resolve().parts[i - 1] return None if __name__ == '__main__': android_map = {} android_root_path = r"C:\Users\f60039632\Desktop\android" ios_root_path = r"C:\Users\f60039632\Desktop\ios" travel_root(ios_root_path) key_value_list_android = travel_root(android_root_path) ios_self_common.extend(ios_self_tai) get_all_differ(key_value_list_android, ios_self_common) thread_remediate_list = threading.Thread(target=write_to_excel, args=(queues_name['remediate_list'], True)) thread_all_differ_key_value = threading.Thread(target=write_to_excel, args=(queues_name['all_differ_key_value'], False)) thread_remediate_list.start() thread_all_differ_key_value.start() write_dict_to_xml(key_value_list_android)
07-04
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值