17、构建类似 Instagram 的应用

构建类似 Instagram 的应用

1. Firebase 初始集成

首先,需要下载 GoogleService-Info.plist 文件,并将其添加到之前创建的标签式应用项目中。完成 Firebase 初始集成的最后一步是在 AppDelegate.swift 中添加以下代码:

import Firebase
FirebaseApp.configure()

代码需添加到以下函数中:

func application(_ application: UIApplication,
     didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?)
     -> Bool

如果一切正确,运行应用时,它将像以前一样启动,控制台会显示相应信息。

2. 实现认证屏幕

每个用户在启动类似 Instagram 的应用时,第一步是进行身份验证。

2.1 登录机制

用户使用移动应用时,需要向服务器和其他用户证明自己的身份。Firebase 会创建用户账户,每个用户至少需要一个账户才能使用该服务,不允许未经授权的访问。单个用户在 Firebase 中创建账户的数量没有限制,唯一要求是拥有有效的电子邮件或手机号码。将虚拟账户与现实世界中的唯一物品或服务关联起来,是减少系统中虚假账户数量的有效方法。我们将使用 Firebase 提供的默认账户创建服务。

Firebase 支持集成最流行的社交网络,如 Facebook 和 Twitter,但不支持 LinkedIn,不过有开源项目支持。此外,也可以像真正的 Instagram 一样使用手机号码登录。

2.2 创建登录屏幕

创建一个全新的视图控制器作为登录屏幕,目前只有使用电子邮件和密码登录这一种选项。如果想使用 Instagram 的标志或资源,需访问 https://en.instagram-brand.com/

登录屏幕有一个标签和一个按钮,该按钮将启动由 Firebase 处理的登录过程。需要将“使用电子邮件登录”屏幕设置为应用的主要入口点,只需将指向标签式视图控制器的箭头指向新的视图控制器。别忘了给新视图控制器设置故事板 ID,例如 SignInViewController ,并给主标签视图控制器命名为 TabbarViewController

Podfile 中添加以下依赖:

pod 'FirebaseUI/Auth', '~> 4.0'

安装依赖并重新打开工作区。创建 SignInViewController.swift 文件,在其中创建 SignInViewController 类,代码如下:

import UIKit
import FirebaseAuthUI

class SignInViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func signInWithEmail(_ sender: Any) {
        let authUI = FUIAuth.defaultAuthUI()
        if let authViewController = authUI?.authViewController() {
            present(authViewController, animated: true, completion: nil)
        }
    }
}

将该类与主故事板中的视图控制器关联,并为“使用电子邮件登录”按钮添加动作。此动作使用默认的 Firebase 流程处理用户登录过程。如果用户已有账户,会提示输入密码;否则,将创建与用户电子邮件关联的新账户。

AppDelegate 中添加额外逻辑,根据用户认证状态切换启动屏幕。如果用户已认证,应显示应用的主屏幕;否则,显示 SignInViewController

class AppDelegate: UIResponder, UIApplicationDelegate, FUIAuthDelegate {
    //....
    func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        FirebaseApp.configure()
        let nc = NotificationCenter.default
        nc.addObserver(forName: Notification.Name(
          rawValue: "userSignedOut"),
                       object: nil, queue: nil) { [weak self]
                        notification in
                        //TODO: remove the stored user information
                        self?.openSingInScreen()
        }
        // handle the successful sign in
        let authUI = FUIAuth.defaultAuthUI()
        authUI?.delegate = self
        let user = Auth.auth().currentUser
        if let user = user {
            save(user: user)
            self.openMainViewController()
        }
        return true
     }

    //MARK:- helper functions
    func save(user: User) {
        //TODO: save the user in memory
    }

    func openSingInScreen() {
        if let signInViewController = self.window?
         .rootViewController?.storyboard?.instantiateViewController(
         withIdentifier: "SignInViewController") as?
         SignInViewController {
            signInViewController.view.frame = (
              self.window?.rootViewController?.view.frame)!
            signInViewController.view.layoutIfNeeded()
            UIView.transition(with: window!, duration: 0.3,
              options: .transitionCrossDissolve, animations: {
                self.window?.rootViewController = signInViewController
            }, completion: { completed in
                // nothing to do here
            })
        }
    }

    func openMainViewController() {
        if let rootViewController = self.window?
         .rootViewController?.storyboard?.instantiateViewController(
         withIdentifier: "TabbarViewController") {
            rootViewController.view.frame = (self.window?
              .rootViewController?.view.frame)!
            rootViewController.view.layoutIfNeeded()
            //nice transition between views
            UIView.transition(with: window!, duration: 0.3,
              options: .transitionCrossDissolve, animations: {
                self.window?.rootViewController = rootViewController
            }, completion: { completed in
                // maybe do something here
            })
        }
    }

    //MARK:- FUIAuthDelegate
    func authUI(_ authUI: FUIAuth, didSignInWith user: User?,
      error: Error?) {
        // handle user and error as necessary
        if let user = user {
            save(user: user)
            self.openMainViewController()
        }
    }
}

上述代码会在 Firebase 报告用户成功认证时触发。如果用户登录过一次,后续每次启动应用时都会自动进行认证。

在检查实现之前,需要在 Firebase 控制台中激活电子邮件/密码登录选项。具体操作是在 Firebase 控制台中选择创建的项目,打开“Authentication”屏幕,点击“Sign-in method”,编辑第一个选项并将其设置为“Enabled”。

3. 应用整体设计

在深入细节之前,我们要对应用的整体体验进行设计,使用包含五个不同部分的标签栏,各部分功能如下:
| 屏幕名称 | 功能描述 |
| ---- | ---- |
| 主页屏幕 | 包含照片的主要信息流,显示你关注的用户的所有新照片 |
| 搜索屏幕 | 用户可以在此搜索不同的内容 |
| 创建帖子屏幕 | 用户可以拍照并应用不同的滤镜,完成后照片将发布,所有用户都能在主页屏幕上看到 |
| 收藏屏幕 | 显示收藏的帖子 |
| 用户个人资料屏幕 | 显示你自己的所有帖子 |

打开故事板,创建三个新的空视图控制器(之前步骤应该已经生成了两个)。在每个视图控制器的中心添加一个标签,并给它一个合适的描述性名称。将这些视图控制器连接到主标签栏,别忘了添加图标,这会让应用更出众。

4. 自定义标签栏按钮

默认状态栏上的按钮可以通过多种方式进行自定义,例如设置自定义图标、更改每个图标下方的文本、添加动画效果,也可以更改颜色。我们希望吸引用户对特定按钮(即触发创建帖子流程的相机按钮)的注意。

为实现这一目标,使用外部开源库 ESTabBarController-swift 。在 Podfile 中添加以下依赖:

pod 'ESTabBarController-swift'

然后更改故事板中与标签栏视图控制器关联的类,创建一个继承自 ESTabbarController 的自定义类 InstagramTabbarController ,代码如下:

import UIKit
import ESTabBarController_swift

class InstagramTabbarController: ESTabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.shouldHijackHandler = {
            tabbarController, viewController, index in
            if index == 2 {
                return true
            }
            return false
        }
        self.didHijackHandler = {
            [weak self] tabbarController, viewController, index in
            DispatchQueue.main.async {
                self?.pesentPicker()
            }
        }
         //update the middle icon
         if let viewController = self.viewControllers?[2] {
             viewController.tabBarItem = ESTabBarItem.init(
             AnimatedContentView(), title: nil, image:
             UIImage(named: "create_post"), selectedImage:
             UIImage(named: "create_post"))
         }
     }

     func pesentPicker() {
         //...
     }
}

这个类重新定义了第三个图标(索引为 2)的行为。默认行为是显示与标签栏视图控制器关联的视图控制器,而我们需要显示一个图像选择器。如果用户没有选择用于帖子的图像,应立即返回原来的位置。

shouldHijackHandler 中,为需要自定义的索引返回 true (这里只有索引 2)。实际按下图标时触发的代码放在 didHijackHandler 中。

使用自定义的 AnimatedContentView 类来实现第三个图标的新外观:

class AnimatedContentView: ESTabBarItemContentView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        iconColor = .black
        highlightIconColor = .white
    }

    public required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public override func highlightAnimation(animated: Bool, completion: (()
-> ())?) {
         UIView.beginAnimations("small", context: nil)
         UIView.setAnimationDuration(0.2)
         let transform = self.imageView.transform.scaledBy(x: 0.8, y: 0.8)
         self.imageView.transform = transform
         UIView.commitAnimations()
         completion?()
     }

    public override func dehighlightAnimation(animated: Bool,
      completion: (() -> ())?) {
         UIView.beginAnimations("big", context: nil)
         UIView.setAnimationDuration(0.2)
         let transform = CGAffineTransform.identity
         self.imageView.transform = transform
         UIView.commitAnimations()
         completion?()
     }
}

highlightAnimation 函数会缩小整个图像,营造按钮被按下的错觉;另一个函数则恢复其初始大小和位置。通过调整这些函数中的数值,可以改变当前的行为。

5. 创建帖子流程

我们将使用外部库 YPImagePicker 来处理渲染用户媒体、选择图像和应用滤镜的复杂过程。这是一个用 Swift 实现类似 Instagram 照片和视频选择器的开源库。在 Podfile 中添加以下依赖:

pod 'YPImagePicker'

然后像之前一样安装新的依赖。

InstagramTabbarController 中,当检测到中间标签栏按钮被点击时,调用 pesentPicker 函数:

func pesentPicker() {
    var config = YPImagePickerConfiguration()
    config.onlySquareImagesFromLibrary = false
    config.onlySquareImagesFromCamera = true
    config.libraryTargetImageSize = .original
    config.usesFrontCamera = true
    config.showsFilters = true
    config.shouldSaveNewPicturesToAlbum = !true
    config.albumName = "IstagramLikeApp"
    config.startOnScreen = .library
    let picker = YPImagePicker(configuration: config)
    picker.didSelectImage = {
             img in
             //TODO: ...
    }
    present(picker, animated: true, completion: nil)
}

如果直接运行应用,它会崩溃,因为 iOS 需要向用户请求访问照片的权限。需要在 Info.plist 文件中定义特殊键,并关联简短的描述文本,让用户知道应用为何需要访问照片库、设备相机和麦克风。具体操作是在 Info.plist 文件中添加以下内容:

<key>NSCameraUsageDescription</key>
<string>InstagramLike app needs access to your camera.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>InstagramLike app needs access to your photos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>InstagramLike app needs access to your microphone.</string>

添加描述后,应用应该可以正常运行,但选择图像并应用滤镜后,无法离开选择器。问题出在 picker.didSelectImage 闭包中,需要在选择图像时手动关闭屏幕。

创建一个新的屏幕来处理添加标题和实际分享帖子的操作。首先在故事板中创建一个新的视图控制器,添加一个宽度和高度均为 110 点的 UIImageView ,旁边放置一个 TextView ,让用户可以为图像添加简短描述。

创建 CreatePostViewController.swift 文件,创建 CreatePostViewController 类并与新视图控制器关联:

class CreatePostViewController: UIViewController {
    override var prefersStatusBarHidden: Bool { return true }
    private let placeholderText = "Write a caption..."
    public var image:UIImage?
    @IBOutlet weak var photo: UIImageView!
    @IBOutlet weak var textView: UITextView! {
        didSet {
            textView.textColor = .gray
            textView.text = placeholderText
            textView.selectedRange = NSRange()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        textView.delegate = self
        photo.image = image
        navigationItem.rightBarButtonItem = UIBarButtonItem(
          title: "Share", style: .done, target: self,
          action: #selector(createPost))
    }

    @objc func createPost() {
        self.dismiss(animated: true, completion: nil)
    }
}

该视图控制器在右上角添加了一个“分享”按钮,点击该按钮应创建新帖子并关闭选择器,让用户回到之前的操作。实际创建帖子并将其保存到 Firebase 服务器的代码将在后续添加。

为了给 TextView 添加占位符文本,在 CreatePostViewController 类中添加以下扩展:

extension CreatePostViewController: UITextViewDelegate {
    func textViewDidChangeSelection(_ textView: UITextView) {
        // Move cursor to beginning on first tap
        if textView.text == placeholderText {
            textView.selectedRange = NSRange()
        }
    }

    func textView(_ textView: UITextView, shouldChangeTextIn range:
NSRange, replacementText text: String) -> Bool {
        if textView.text == placeholderText && !text.isEmpty {
            textView.text = nil
            textView.textColor = .black
            textView.selectedRange = NSRange()
        }
        return true
    }

    func textViewDidChange(_ textView: UITextView) {
        if textView.text.isEmpty {
            textView.textColor = .gray
            textView.text = placeholderText
        }
    }
}

TextView 根据用户输入的内容决定显示什么文本。如果文本为空,显示占位符文本;如果用户输入了任何文本,则显示该文本,并相应地管理选择。

最后,在 InstagramTabbarController didSelectImage 闭包中将新的 CreatePostViewController 与选择器关联起来:

picker.didSelectImage = {
    [unowned picker, weak self] img in
    if let viewController = self?.storyboard?
      .instantiateViewController(withIdentifier: "CreatePost")
      as? CreatePostViewController {
        viewController.image = img
        // Use Fade transition instead of default push animation
        let transition = CATransition()
        transition.duration = 0.3

通过以上步骤,我们逐步构建了一个类似 Instagram 的应用,包括 Firebase 集成、用户认证、应用整体设计以及自定义标签栏和创建帖子的功能。后续还可以进一步完善和扩展应用的功能。

构建类似 Instagram 的应用(下半部分)

6. 连接创建帖子视图控制器

InstagramTabbarController didSelectImage 闭包中,我们已经开始将 CreatePostViewController 与选择器关联起来。接下来,我们要完成这个关联过程,确保用户选择图片后能顺利进入创建帖子的界面。

picker.didSelectImage = {
    [unowned picker, weak self] img in
    if let viewController = self?.storyboard?
      .instantiateViewController(withIdentifier: "CreatePost")
      as? CreatePostViewController {
        viewController.image = img
        // Use Fade transition instead of default push animation
        let transition = CATransition()
        transition.duration = 0.3
        transition.type = .fade
        self?.view.window?.layer.add(transition, forKey: kCATransition)
        self?.present(viewController, animated: false, completion: nil)
    }
    picker.dismiss(animated: true, completion: nil)
}

在上述代码中,我们完成了以下操作:
1. 实例化 CreatePostViewController 并将选择的图片赋值给它。
2. 使用 CATransition 创建一个淡入淡出的过渡效果。
3. 显示 CreatePostViewController
4. 关闭图片选择器。

7. 保存帖子到 Firebase

目前, CreatePostViewController 中的 createPost 方法只是简单地关闭了视图控制器,我们需要实现将帖子保存到 Firebase 的功能。

首先,确保已经在项目中导入了 Firebase 的存储和数据库模块:

import FirebaseStorage
import FirebaseDatabase

然后,修改 CreatePostViewController 中的 createPost 方法:

@objc func createPost() {
    guard let image = image, let caption = textView.text, !caption.isEmpty else {
        return
    }

    // 上传图片到 Firebase Storage
    let storageRef = Storage.storage().reference().child("posts/\(UUID().uuidString).jpg")
    if let uploadData = image.jpegData(compressionQuality: 0.8) {
        storageRef.putData(uploadData, metadata: nil) { (metadata, error) in
            if let error = error {
                print("Error uploading image: \(error)")
                return
            }

            // 获取图片的下载 URL
            storageRef.downloadURL { (url, error) in
                if let error = error {
                    print("Error getting download URL: \(error)")
                    return
                }

                guard let imageURL = url?.absoluteString else {
                    return
                }

                // 保存帖子信息到 Firebase Database
                let postData = [
                    "caption": caption,
                    "imageURL": imageURL,
                    "timestamp": ServerValue.timestamp()
                ] as [String : Any]

                let databaseRef = Database.database().reference().child("posts")
                let newPostRef = databaseRef.childByAutoId()
                newPostRef.setValue(postData) { (error, ref) in
                    if let error = error {
                        print("Error saving post: \(error)")
                    } else {
                        self.dismiss(animated: true, completion: nil)
                    }
                }
            }
        }
    }
}

上述代码的执行流程如下:
1. 检查图片和标题是否存在。
2. 生成一个唯一的文件名,将图片上传到 Firebase Storage。
3. 获取图片的下载 URL。
4. 将帖子信息(标题、图片 URL 和时间戳)保存到 Firebase Database。
5. 关闭 CreatePostViewController

8. 显示帖子列表

在主页屏幕上,我们需要显示所有用户的帖子列表。创建一个新的视图控制器 HomeViewController ,并在故事板中关联它。

首先,在 HomeViewController 中导入必要的模块:

import UIKit
import FirebaseDatabase
import SDWebImage

然后,实现 UITableViewDataSource UITableViewDelegate 协议:

class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    @IBOutlet weak var tableView: UITableView!
    var posts = [Post]()

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.dataSource = self
        tableView.delegate = self

        // 从 Firebase Database 获取帖子数据
        let databaseRef = Database.database().reference().child("posts")
        databaseRef.observe(.value) { (snapshot) in
            self.posts.removeAll()
            for child in snapshot.children {
                if let postSnapshot = child as? DataSnapshot,
                   let postData = postSnapshot.value as? [String: Any],
                   let caption = postData["caption"] as? String,
                   let imageURL = postData["imageURL"] as? String,
                   let timestamp = postData["timestamp"] as? TimeInterval {
                    let post = Post(caption: caption, imageURL: imageURL, timestamp: timestamp)
                    self.posts.append(post)
                }
            }
            self.posts.sort { $0.timestamp > $1.timestamp }
            self.tableView.reloadData()
        }
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return posts.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "PostCell", for: indexPath) as! PostTableViewCell
        let post = posts[indexPath.row]

        cell.captionLabel.text = post.caption
        cell.timestampLabel.text = Date(timeIntervalSince1970: post.timestamp).timeAgoDisplay()
        cell.postImageView.sd_setImage(with: URL(string: post.imageURL), placeholderImage: UIImage(named: "placeholder"))

        return cell
    }
}

struct Post {
    let caption: String
    let imageURL: String
    let timestamp: TimeInterval
}

extension Date {
    func timeAgoDisplay() -> String {
        let formatter = RelativeDateTimeFormatter()
        formatter.unitsStyle = .full
        return formatter.localizedString(for: self, relativeTo: Date())
    }
}

上述代码的主要功能如下:
1. 从 Firebase Database 获取所有帖子数据。
2. 将帖子数据存储在 posts 数组中,并按时间戳排序。
3. 使用 UITableView 显示帖子列表。
4. 在单元格中显示帖子的标题、图片和时间戳。

9. 总结

通过以上步骤,我们完成了一个类似 Instagram 的应用的主要功能开发,包括:
1. Firebase 集成和用户认证。
2. 自定义标签栏和创建帖子功能。
3. 图片选择和滤镜应用。
4. 帖子保存到 Firebase Storage 和 Database。
5. 主页屏幕显示帖子列表。

以下是整个应用开发流程的 mermaid 流程图:

graph LR
    A[Firebase 初始集成] --> B[实现认证屏幕]
    B --> C[应用整体设计]
    C --> D[自定义标签栏按钮]
    D --> E[创建帖子流程]
    E --> F[连接创建帖子视图控制器]
    F --> G[保存帖子到 Firebase]
    G --> H[显示帖子列表]

这个应用还有很多可以扩展和优化的地方,例如添加用户个人资料页面、点赞和评论功能、推送通知等。希望这篇文章能帮助你构建出一个功能丰富的类似 Instagram 的应用。

本 PPT 介绍了制药厂房中供配电系统的总体概念与设计要点,内容包括: 洁净厂房的特点及其对供配电系统的特殊要求; 供配电设计的一般原则与依据的国家/行业标准; 从上级电网到工厂变电所、终端配电的总体结构与模块化设计思路; 供配电范围:动力配电、照明、通讯、接地、防雷与消防等; 动力配电中电压等级、接地系统形式(如 TN-S)、负荷等级与可靠性、UPS 配置等; 照明的电源方式、光源选择、安装方式、应急与备用照明要求; 通讯系统、监控系统在生产管理与消防中的作用; 接地与等电位连接、防雷等级与防雷措施; 消防设施及其专用供电(消防泵、排烟风机、消防控制室、应急照明等); 常见高压柜、动力柜、照明箱等配电设备案例及部分设计图纸示意; 公司已完成的典型项目案例。 1. 工程背景与总体框架 所属领域:制药厂房工程的公用工程系统,其中本 PPT 聚焦于供配电系统。 放在整个公用工程中的位置:与给排水、纯化水/注射用水、气体与热力、暖通空调、自动化控制等系统并列。 2. Part 01 供配电概述 2.1 洁净厂房的特点 空间密闭,结构复杂、走向曲折; 单相设备、仪器种类多,工艺设备昂贵、精密; 装修材料与工艺材料种类多,对尘埃、静电等更敏感。 这些特点决定了:供配电系统要安全可靠、减少积尘、便于清洁和维护。 2.2 供配电总则 供配电设计应满足: 可靠、经济、适用; 保障人身与财产安全; 便于安装与维护; 采用技术先进的设备与方案。 2.3 设计依据与规范 引用了大量俄语标准(ГОСТ、СНиП、SanPiN 等)以及国家、行业和地方规范,作为设计的法规基础文件,包括: 电气设备、接线、接地、电气安全; 建筑物电气装置、照明标准; 卫生与安全相关规范等。 3. Part 02 供配电总览 从电源系统整体结构进行总览: 上级:地方电网; 工厂变电所(10kV 配电装置、变压
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值