ToB企服应用市场:ToB评测及商务社交产业平台
标题:
iOS高级开辟技术与实例代码解析
[打印本页]
作者:
十念
时间:
2025-1-12 00:19
标题:
iOS高级开辟技术与实例代码解析
本文还有配套的佳构资源,点击获取
简介:本资源包聚集了多个代码实例,涵盖了iOS高级开辟中的关键技术和实践。每个实例针对不同的高级主题,包罗多线程编程、自动结构、数据持久化、网络编程、SwiftUI、动画制作、推送通知、地图定位服务、手势辨认以及本地和长途通知。这些实例旨在资助开辟者深入明白高级技术,并在现实项目中应用,提升开辟服从和应用质量。
1. 多线程编程实践
随着移动应用和后端服务日益复杂,多线程编程成为了提高性能和响应速度的关键技术。本章将探讨多线程编程的实践方法,包罗线程的创建、管理以及资源同步机制。
1.1 多线程的基本概念和优势
多线程指的是在同一时间片内,多个线程可以并行实行不同的使命。在多核处置惩罚器上,这可以显着提升步伐的服从,由于它允许更合理地利用硬件资源。多线程编程的优势在于提高应用的运行速度,改善用户体验,同时实行多个操作而不会导致界面冻结。
1.2 创建和管理线程的最佳实践
在iOS开辟中,可以使用Grand Central Dispatch(GCD)和Operation Queue来创建和管理线程。这两种方法都可以简单高效地实现线程的异步实行。代码示比方下:
let queue = DispatchQueue(label: "com.myapp.myqueue")
queue.async {
// 执行后台任务
}
queue.sync {
// 等待后台任务完成
}
复制代码
管理线程时,必要注意克制死锁和线程竞争标题。线程同步机制如串行队列、信号量、栅栏等可以资助控制线程间的安全访问。
1.3 线程间通信与资源共享
线程之间举行数据互换时,必须使用线程安全的通信机制。常见的方法包罗使用串行队列通报数据、使用锁和信号量控制对共享资源的访问。比方,使用NSLock或 OSSpinLock来克制竞态条件,确保数据的同等性。代码示例:
let lock = NSLock()
lock.lock()
// 访问共享资源
lock.unlock()
复制代码
在多线程编程实践中,合理管理线程的生命周期和资源分配是提升服从和保证应用稳定的重中之重。接下来的章节会具体介绍自动结构、Core Data等其他重要技术。
2. 自动结构与Size Classes应用
2.1 自动结构基础
2.1.1 自动结构的概念和作用
自动结构(Auto Layout)是Apple公司在iOS开辟中引入的一种高效结构机制,用于替换传统的frame和bounds结构。它允许开辟者通过声明式的界面描述来界说视图间的相对关系和约束(constraints),而不是硬编码的绝对位置。这种结构方式的核心是约束,它使得结构能够根据屏幕大小、设备方向和字体大小的变化举行自动调整。
自动结构的优势在于它提供了更高的机动性和适应性,确保了应用界面在不同设备和屏幕尺寸上能够具有同等的用户体验。通过使用自动结构,开辟者可以减少大量针对不同屏幕尺寸的手动适配工作,大大提高了开辟服从和应用的可维护性。
2.1.2 约束的创建和调试技巧
在iOS开辟中,创建约束可以手动编写代码,也可以通过Interface Builder(IB)举行可视化操作。无论是哪种方式,都必要对约束的创建有一个清晰的认识。
在代码中,通常会使用NSLayoutConstraint类来创建约束。比如,若要创建一个视图与父视图的顶部和左侧对齐的约束,可以这样编写代码:
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false // 必须先禁用手动布局
view.widthAnchor.constraint(equalToConstant: 100).isActive = true // 宽度为100点
view.heightAnchor.constraint(equalToConstant: 50).isActive = true // 高度为50点
view.leadingAnchor.constraint(equalTo: view.superview!.leadingAnchor).isActive = true // 左侧对齐
view.topAnchor.constraint(equalTo: view.superview!.topAnchor).isActive = true // 顶部对齐
复制代码
当使用Interface Builder时,开辟者可以通过拖拽视图之间的连接线来创建约束,或者使用工具栏上的按钮来添加约束。在IB中,约束被自动添加到文件的所有者(通常是对应的ViewController),而且可以通过约束检查器来设置常数、优先级等属性。
调试约束的常见标题包罗约束冲突和约束不敷。当发生冲突时,Interface Builder会显示红色的约束线,表示约束之间存在冲突。而约束不敷则大概导致视图结构不正确。解决这些标题通常必要检查约束的优先级和必要条件,确保结构在所有环境下都能正确工作。
2.2 Size Classes深入明白
2.2.1 Size Classes的界说和应用场景
Size Classes是iOS 8中引入的一个概念,用于描述不同设备和方向下的结构尺寸,它界说了三个尺寸:紧凑(Compact)、通例(Regular)和未指定(Any)。Size Classes允许开辟者为不同尺寸的设备和方向创建更加机动的界面结构。
紧凑尺寸(Compact)用于小屏设备或者竖屏模式下的平板设备,通例尺寸(Regular)用于大屏设备或者横屏模式下的平板设备,而未指定尺寸(Any)则可以用于所有范例的设备。通过这些Size Classes,开辟者能够界说适用于不同环境的结构,而不必要为每一种设备和方向编写特定的结构代码。
在Interface Builder中,Size Classes可以与自动结构结合使用,通过设置特定的Size Classes样式来设计结构。当设备的Size Classes与设计的尺寸不匹配时,系统会自动应用相应的结构,从而实现了对不同尺寸设备的适应。
2.2.2 不同Size Classes下的结构适配策略
在处置惩罚不同Size Classes的适配策略时,开辟者必要明白在哪些环境下必要设计特定的结构。比方,当应用在竖屏和横屏下的结构必要完全不同的设计时,就必要为相应的方向设计特定的结构。
使用Interface Builder时,开辟者可以通过创建不同的Size Class故事板(Storyboards)来管理不同环境下的结构。在Xcode的工具栏中,开辟者可以选择不同的设备和方向组合,然后根据这个组合来设计结构。
对于代码实现,可以利用traitCollection来动态检测当前设备的Size Classes信息,并根据这些信息来调整结构。
override func viewDidLoad() {
super.viewDidLoad()
if UIDevice.current.userInterfaceIdiom == .phone {
switch traitCollection.horizontalSizeClass {
case .compact:
// 竖屏下紧凑尺寸的布局调整
case .regular:
// 竖屏下常规尺寸的布局调整
default:
break
}
} else {
// 平板设备的布局调整
}
}
复制代码
通过使用Size Classes,开辟者能够实现更加精致的用户界面结构适配,让应用在所有范例的设备上都能提供最佳的用户体验。
3. Core Data框架使用
Core Data作为iOS开辟中一个强大的数据持久化解决方案,它封装了SQLite数据库、内存中的对象图和撤销管理的功能,使得开辟者能够更加专注于数据模型的设计。本章将从Core Data的基础入门开始,逐步介绍模型设计和持久化存储,再深入探讨其高级技巧,包罗进阶查询、性能优化,以及数据迁移与版本管理。
3.1 Core Data基础入门
3.1.1 Core Data框架简介
Core Data是苹果公司提供的一个用于管理数据的框架,它允许开辟者通过界说数据模型、实行查询、更新数据和管理数据上下文来对数据举行操作。与传统的数据库操作不同,Core Data通过NSManagedObject的子类,将数据封装为对象,使得数据操作更加面向对象。
核心特性包罗: - 数据模型的创建和管理 - 对象的CRUD操作(创建、读取、更新、删除) - 数据变动通知机制 - 内存中对象和数据库之间的同步
3.1.2 模型设计和持久化存储
Core Data的数据模型由一个名为.xcdatamodeld的文件表示,此中界说了实体(Entity)和属性(Attribute)等概念。实体代表数据存储中的一组记录,属性则是实体的字段,可以是整型、字符串、日期或其他范例。
持久化存储则涉及到将内存中的对象持久化到硬盘上的文件,这通常是通过NSManagedObjectContext来完成的。一个上下文(Context)负责管理一组对象的生命周期,以及处置惩罚这些对象的变化,最终将变化持久化到持久化存储和谐器(NSPersistentStoreCoordinator)所管理的存储中。
// 示例代码:如何创建一个Core Data的上下文
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedContext = appDelegate.persistentContainer.viewContext
// 示例代码:创建一个新的managed object
let newObject = NSEntityDescription.insertNewObject(forEntityName: "Person", into: managedContext)
newObject.setValue("John Doe", forKey: "name")
newObject.setValue(30, forKey: "age")
do {
try managedContext.save()
} catch {
print("Error saving context: \(error)")
}
复制代码
3.2 Core Data高级技巧
3.2.1 进阶查询和性能优化
Core Data提供了多种方式来实行复杂的数据查询。开辟者可以使用NSFetchRequest来获取数据聚集,使用NSPredicate来添加查询过滤条件,以及使用NSSortDescriptor来对结果举行排序。
在处置惩罚大量数据或者复杂查询时,性能成为一个关键因素。有用的优化策略包罗: - 优化数据模型设计,确保仅存储必要的数据 - 使用预编译的NSPredicate,克制动态创建导致的性能损耗 - 在多线程环境下合理管理上下文 - 使用(fetchBatchSize
对大量数据举行分批加载,减少内存消耗
// 示例代码:使用NSPredicate进行数据查询
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Person")
let predicate = NSPredicate(format: "age > 50")
fetchRequest.predicate = predicate
do {
let results = try managedContext.fetch(fetchRequest) as! [NSManagedObject]
// 处理查询结果
} catch {
print("Error fetching data: \(error)")
}
复制代码
3.2.2 数据迁移与版本管理
随着应用的发展,数据模型往往会发生变化,比方添加或删除属性、改变实体名称等。Core Data支持数据模型的迁移,使得开辟者能够在应用升级时保持用户数据的完备性。
数据迁移分为轻量级迁移和重量级迁移两种: - 轻量级迁移可以处置惩罚大多数的简单数据模型变动,仅必要在.xcdatamodeld文件中更新模型并设置正确的模型版本。 - 重量级迁移则涉及到更复杂的数据转换和映射模型文件(.mom),必要更多的手动设置和数据转换逻辑。
// 示例代码:设置Core Data的模型版本
let modelURL = Bundle.main.url(forResource: "Model", withExtension: "momd")
let model = try NSManagedObjectModel(contentsOf: modelURL!)
let coordinator = persistentStoreCoordinator
let options: [String: Any] = [
NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true
]
let storeURL = ... // 指定持久化存储文件URL
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, URL: storeURL, options: options)
} catch {
print("Error adding persistent store: \(error)")
}
复制代码
通过本章节的介绍,我们相识了Core Data的基础概念、模型设计、以及数据的持久化存储。同时,我们通过代码示例学习了如何使用Core Data举行简单的数据操作和进阶的数据查询。最后,我们探讨了数据迁移与版本管理的概念,为之后深入学习和实践Core Data打下了坚实的基础。
4. 网络请求与JSON解析
网络请求和JSON解析是现代移动应用开辟中不可或缺的技能之一,尤其是在开辟必要实时数据交互的应用步伐时。这一章节将深入探讨如何使用URLSession构建网络请求,并解析JSON数据。
4.1 网络请求实现
4.1.1 使用URLSession构建网络请求
在Swift中,Apple推荐使用 URLSession 类来处置惩罚HTTP请求。 URLSession 是iOS SDK提供的一个强大的网络通信工具,它可以用来创建数据使命、下载使命和上传使命。
示例代码
下面的示例代码展示了如何使用 URLSession 创建一个简单的GET请求,以获取网络上的数据。
let url = URL(string: "https://api.example.com/data")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("请求失败: \(error)")
return
}
guard let data = data else { return }
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: [])
print("解析的数据: \(jsonResult)")
} catch let parseError {
print("解析失败: \(parseError)")
}
}
task.resume()
复制代码
参数分析
URL(string
:创建一个 URL 实例,表示要请求的网络所在。
dataTask(with:completionHandler
:创建一个数据使命,用于发起网络请求。
completionHandler :这是一个闭包,用于处置惩罚请求完成后的数据、响应和大概发生的错误。
实行逻辑分析
在实行逻辑中,首先通过网络所在创建一个URL实例,然后使用 URLSession 的 dataTask 方法发起GET请求。请求完成后,会在 completionHandler 中处置惩罚结果,此中包罗对 data 的处置惩罚来解析JSON数据。
4.1.2 错误处置惩罚和网络状态监控
网络请求过程中,错误处置惩罚是确保应用稳定运行的关键。Swift提供了 URLError 枚举来标识大概发生的URL错误,比方无法解析URL、连接超时或网络不可用等。
错误处置惩罚
task.resume()
task.cancel()
task完成了 = false
var lastError: Error?
while !task完成了 {
// 等待任务完成
}
if let error = lastError {
switch error as? URLError {
case .timedOut:
print("请求超时")
case .notConnectedToInternet:
print("网络不可用")
default:
print("请求失败: \(error)")
}
}
复制代码
网络状态监控
除了处置惩罚错误之外,应用还必要监控网络状态变化,以适应网络环境的变化。可以使用 NWPathMonitor 来监控网络路径的变化。
let monitor = NWPathMonitor()
monitor.start(queue: .main)
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
// 网络可用,可以发起网络请求
} else {
// 网络不可用
}
}
复制代码
表格:网络请求错误范例
| 错误枚举值 | 描述 | |----------------------|--------------------------------| | .timedOut | 请求超时 | | .notConnectedToInternet | 没有连接到互联网 | | .hostNotFound | 主机名找不到 | | .httpStatus | 服务器返回HTTP状态码错误 |
4.2 JSON数据解析
4.2.1 JSON数据结构和解析方法
JSON(JavaScript Object Notation)是一种轻量级的数据互换格式,易于人阅读和编写,也易于机器解析和天生。在移动开辟中,通常必要从服务器获取JSON格式的数据,并在应用中将其解析成相应的数据模型。
JSON结构
一个典型的JSON结构如下所示:
{
"name": "John",
"age": 30,
"car": null,
"phones": [
"+44 1234567",
"+44 2345678"
]
}
复制代码
解析方法
在Swift中,可以使用 JSONSerialization 类提供的 jsonObject(with
ptions
方法来将JSON数据解析为Swift中的数据范例(如字典、数组等)。
do {
if let data = try Data(contentsOf: url),
let jsonResult = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
// 解析成功,处理jsonResult
}
} catch {
print("解析JSON失败: \(error)")
}
复制代码
4.2.2 与后端数据交互的实践案例
下面给出一个与后端举行数据交互的实践案例。
示例代码
func fetchData() {
let url = URL(string: "https://api.example.com/data")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
print("请求失败: \(error)")
return
}
guard let data = data else { return }
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print("获取的数据: \(jsonResult)")
} catch let parseError {
print("解析失败: \(parseError)")
}
}
task.resume()
}
复制代码
逻辑分析
此函数实行一个GET请求来从服务器获取数据。网络请求完成后,将返回的数据解析为JSON格式,并打印出来。这里使用了 JSONSerialization 的 jsonObject(with
ptions
方法,并使用 .allowFragments 选项允许解析JSON中的片断。
参数分析
.allowFragments :此选项允许 JSONSerialization 处置惩罚JSON片断,这在处置惩罚部分响应或者分段的数据时很有用。
交互性讨论
在现实的开辟工作中,解析JSON数据经常陪同着对错误处置惩罚的增强,比方:
对于大概缺失的字段,举行可选绑定处置惩罚。
对于数据模型的校验,确保解析后的数据符合预期格式。
对于错误的处置惩罚,可以引入日记记录,便于标题追踪和解决。
通过以上方法,可以有用地处置惩罚网络请求和JSON数据解析的相干标题,从而提高应用的结实性和用户体验。
5. SwiftUI界面构建技术
随着SwiftUI的推出,Apple引入了一种全新声明式的UI编程范式,大大简化了界面开辟流程。本章将探讨SwiftUI的基本组件应用和进阶结构策略,为开辟者提供构建直观、动态界面的深度技术引导。
5.1 SwiftUI基本组件应用
5.1.1 SwiftUI视图的基本构建块
在SwiftUI中,视图(View)是构建UI的基本单元。不同于UIKit的控件,SwiftUI的视图是轻量级的描述性结构,能够以声明式的方式组合在一起。一个基本的SwiftUI视图可以通过一个遵循 View 协议的结构体来界说。比方,一个简单的文本视图(Text)可以这样声明:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, SwiftUI!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
复制代码
上述代码创建了一个显示“Hello, SwiftUI!”的简单文本视图。在这里, body 属性界说了视图的内容。SwiftUI还提供了许多内置的视图范例,如 Image , Button , Spacer , Divider 等,开辟者可以根据必要组合这些视图来构建复杂的界面。
5.1.2 动画和过渡效果的实现
SwiftUI另一个亮点是内置的声明式动画支持。开辟者可以轻松地为视图添加动画效果,而无需编写复杂的动画代码。比方,下面的代码演示了如作甚一个视图添加简单的旋转动画:
struct RotatingView: View {
@State private var angle: Double = 0.0
var body: some View {
Image(systemName: "arrow.2.circlepath.circle.fill")
.rotationEffect(Angle(degrees: angle))
.onAppear() {
withAnimation {
self.angle += 360
}
}
}
}
复制代码
在这个例子中, onAppear() 修饰符用于在视图出现时应用动画。 withAnimation 块内的代码将使图像旋转一整圈。使用 @State 属性包装器,可以在视图之间共享并修改状态,进而控制动画的行为。
5.2 SwiftUI进阶结构策略
5.2.1 自界说视图和结构算法
SwiftUI允许开辟者创建自界说视图和结构。通过继承 View 协议并实现 body 属性,开辟者可以设计出完全符合本身需求的视图组件。下面是一个简单的自界说视图示例:
struct CustomView: View {
var color: Color
var body: some View {
Rectangle()
.fill(color)
.cornerRadius(10)
}
}
复制代码
自界说视图不仅限于显示基本图形,还可以包含复杂的结构逻辑。利用 VStack , HStack , ZStack 等结构视图,开辟者可以创建出清晰、井井有条的界面。SwiftUI的响应式编程模型使得结构可以根据数据变化而动态调整。
5.2.2 利用SwiftUI举行响应式编程
响应式编程(Reactive Programming)在SwiftUI中扮演着核心角色。开辟者不必要显式地管理视图的更新,而是通过声明式地描述UI应该在何种条件下显示何种内容。比方,使用 ForEach 可以基于数组动态天生视图列表,并自动响应数组内容的变化。
import SwiftUI
struct ContentView: View {
let items = ["Item 1", "Item 2", "Item 3"]
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text(item)
}
}
}
}
复制代码
在上述代码中, List 和 ForEach 结合使用,根据 items 数组的内容动态天生了三个文本项。如果 items 数组中的内容发生变化,比如添加或删除了某个项目,那么UI也会相应地更新,无需额外的代码干预。
SwiftUI的响应式特性不仅限于处置惩罚数组,还可以与状态管理工具如 @State 和 @ObservableObject 等相结合,创建更为复杂的响应式应用步伐。通过这种方式,SwiftUI将UI的声明性和响应式编程的优势发挥到了极致,极大地简化了界面开辟流程。
6. Core Animation动画制作
6.1 动画基础与关键帧动画
6.1.1 Core Animation框架概述
Core Animation是Apple为开辟者提供的一个强大的框架,可以用来创建流畅且吸引人的动画效果。框架的核心是图层(CALayer),每个视图(UIView)背后都有一个或多个图层。通过调整这些图层的属性,我们可以创建出从简单的淡入淡出到复杂的转场和3D变动的各种动画效果。
在创建动画前,我们应该明确动画的目标和作用,是用于吸引用户注意力、引导用户界面操作,还是为了提供视觉反馈。好的动画能够提升用户体验,而不恰当的动画则大概导致用户混淆。
6.1.2 关键帧动画的创建和控制
关键帧动画是动画中的一种基本技术,它允许我们指定一系列的关键帧,动画系统会在这些帧之间举行插值来天生动画效果。使用 CAKeyframeAnimation 类,我们可以针对不同的属性设置关键帧动画,比方位置、透明度、旋转等。
下面是一个简单的关键帧动画示例,我们创建一个视图的旋转动画:
let keyFrameAnimation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
keyFrameAnimation.values = [0, .pi / 2, .pi, 3 * .pi / 2, 2 * .pi] // 旋转角度从0到2π
keyFrameAnimation.keyTimes = [0, 0.25, 0.5, 0.75, 1.0] // 每个关键帧的动画时间比例
keyFrameAnimation.duration = 2 // 整个动画的持续时间
view.layer.add(keyFrameAnimation, forKey: nil)
复制代码
在这个例子中, values 数组界说了旋转的关键帧,而 keyTimes 界说了每个关键帧所占的时间比例,这样可以控制每个阶段动画的速度。 duration 属性设定了动画的总时长。最后,将动画添加到视图的图层上。
6.2 高级动画技术和性能优化
6.2.1 运动模糊和3D动画效果
运动模糊是动画中增加快度感的一种常用技巧,Core Animation提供了 CABasicAnimation 类来实现运动模糊效果。要创建运动模糊效果,我们可以通过修改图层的滤镜属性实现。
以下是如作甚一个图层添加运动模糊效果的示例:
let animation = CABasicAnimation(keyPath: "filters")
animation.fromValue = nil
animation.toValue = [CIFilter(name: "CIMotionBlur")!]
animation.duration = 1
view.layer.add(animation, forKey: "motionBlur")
复制代码
3D动画效果可以为用户界面带来更丰富的视觉体验。 CAReplicatorLayer 和 CATransform3D 是实现3D效果的关键技术。通过3D变动,我们可以在X、Y、Z三个维度上对图层举行旋转、缩放和倾斜。
6.2.2 动画性能监控和优化策略
动画性能对用户体验至关重要,卡顿和延迟都会影响用户体验。为了优化动画性能,我们可以举行以下几个方面的操作:
减少动画中图层的数量。
减少动画期间视图的重绘次数,比方通过使用不透明图层来克制透明度的绘制。
尽量使用视图的 transform 属性而不是改变视图的 frame 。
对复杂动画使用视图层次结构的裁剪。
我们还可以使用CADisplayLink来检测和监控动画性能。CADisplayLink是一个定时器,它在显示器革新时触发,这可以资助我们同步动画的更新与屏幕的革新。
以下是创建一个CADisplayLink用于动画性能监控的例子:
let displayLink = CADisplayLink(target: self, selector: #selector(didRefreshDisplayLink))
displayLink.add(to: .current, forMode: .default)
@objc func didRefreshDisplayLink(displayLink: CADisplayLink) {
// 更新动画
// 在这里添加动画逻辑,以保持与屏幕刷新同步
}
复制代码
通过上述策略,我们可以更好地控制动画性能,确保用户界面流畅运行。不外,要注意在现实应用中举行充实的测试,确保动画的流畅性和可靠性。
本文还有配套的佳构资源,点击获取
简介:本资源包聚集了多个代码实例,涵盖了iOS高级开辟中的关键技术和实践。每个实例针对不同的高级主题,包罗多线程编程、自动结构、数据持久化、网络编程、SwiftUI、动画制作、推送通知、地图定位服务、手势辨认以及本地和长途通知。这些实例旨在资助开辟者深入明白高级技术,并在现实项目中应用,提升开辟服从和应用质量。
本文还有配套的佳构资源,点击获取
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4