Commit d68129c2 authored by yqz's avatar yqz

iniit

parent 7dadafe9
......@@ -12,7 +12,7 @@ target 'SpeakEasyLearnEnglish' do
pod 'lottie-ios', '~> 4.5.2'
pod 'SnapKit', '~> 5.7.1'
pod 'YYText'
pod 'TPKeyboardAvoiding' , '~> 1.3.5'
post_install do |installer|
installer.pods_project.targets.each do |target|
......
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Send@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Send@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Icon Name@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Icon Name@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Icon Name@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Icon Name@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
//
// AppDelegate+LoadData.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
extension AppDelegate {
func initData() -> Void {
SpeakKeyboardManager.share.monitor()
_ = SpeakVideoPlayer.share
SpeakElePublicManager.share.loadData()
}
}
......@@ -15,7 +15,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
_ = SpeakVideoPlayer.share
initData()
#if DEBUG
// // 获取所有可用字体家族名称
......@@ -45,6 +46,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
......@@ -12,6 +12,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
internal var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = SpeakEleLaunchViewCtr()
......@@ -34,6 +35,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
func sceneWillResignActive(_ scene: UIScene) {
saveData()
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
......@@ -49,6 +51,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// to restore the scene back to its current state.
}
func saveData() -> Void {
SpeakElePublicManager.share.saveData()
}
}
......@@ -44,4 +44,37 @@ class SpeakEleBaseNavigationCtr: UINavigationController, UIGestureRecognizerDele
return true
}
func navigationController(_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationController.Operation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if toVC.navigationController?.AnimationState == .present {
return SpeakCustomPushAnimator()
}
return nil
}
}
fileprivate var state = SpeakEasyAssociatedKeys.navigationAniKey.rawValue
extension UINavigationController {
enum navigationState : Int {
case none = 0
case present = 1
}
var AnimationState:navigationState {
set {
objc_setAssociatedObject(self, &state, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
let animation = objc_getAssociatedObject(self, &state) as? navigationState ?? .none
return animation
}
}
}
......@@ -16,19 +16,25 @@ let isDebug = false
let marginLR = 24
// MARK: - 所有分类属性 Key
enum SpeakEasyAssociatedKeys : UInt32 {
case fontsize = 0x101010
case UnsafeCallBack = 0x10000
case labelTextTap = 0x10001
case navigationAniKey = 0x01122
case labelTextCutomFont = 0x01102
}
protocol CutomFontProtocol {
protocol CutomFontProtocol : NSObjectProtocol {
var fontSize:CGFloat { get set }
var cutomFont:Int { get }
func setCutomFonts() -> Void
}
// MARK: - userdefaultKey
enum UnsafeRawUserDefaultsKey:String {
case UnsafeThreeDayGuide = "UnsafeThreeDayGuide"
case UnsafePublicDataKey = "UnsafePublicDataKey"
}
......@@ -10,6 +10,14 @@ import UIKit
extension String {
func localJson(_ fileName:String) -> String? {
let path = Bundle.main.path(forResource: fileName, ofType: "json")
return path
}
func attributed() -> AttributedStringBuilder {
return AttributedStringBuilder(self)
}
......
......@@ -9,22 +9,9 @@ import UIKit
extension UIButton : CutomFontProtocol {
@IBInspectable var fontSize:CGFloat {
set {
var state = SpeakEasyAssociatedKeys.fontsize.rawValue
objc_setAssociatedObject(self, &state, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
var state = SpeakEasyAssociatedKeys.fontsize.rawValue
let size = objc_getAssociatedObject(self, &state) as? CGFloat
return size ?? 12
}
}
@IBInspectable var cutomFont:Int {
set {
switch newValue {
func setCutomFonts() {
if cutomFont >= 0 {
switch cutomFont {
case 0:
self.titleLabel?.font = UIFont.montserrat(.regular ,size: fontSize)
break
......@@ -39,10 +26,6 @@ extension UIButton : CutomFontProtocol {
break
}
}
get {
return 0
}
}
}
......@@ -8,11 +8,12 @@
import UIKit
import CoreText
fileprivate var state = SpeakEasyAssociatedKeys.fontsize.rawValue
fileprivate var textTapState = SpeakEasyAssociatedKeys.labelTextTap.rawValue
@IBDesignable
extension UILabel : CutomFontProtocol{
extension UILabel : CutomFontProtocol {
/// 设置文字间距
func lineSpacing(_ lineSpacing:CGFloat = 5) {
......@@ -24,19 +25,10 @@ extension UILabel : CutomFontProtocol{
}
@IBInspectable var fontSize:CGFloat {
set {
objc_setAssociatedObject(self, &state, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
let size = objc_getAssociatedObject(self, &state) as? CGFloat
return size ?? 12
}
}
@IBInspectable var cutomFont:Int {
set {
switch newValue {
func setCutomFonts() {
if cutomFont >= 0 {
switch cutomFont {
case 0:
self.font = UIFont.montserrat(.regular ,size: fontSize)
break
......@@ -51,12 +43,8 @@ extension UILabel : CutomFontProtocol{
break
}
}
get {
return 0
}
}
var textTaps:[String] {
set {
objc_setAssociatedObject(self, &textTapState, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
......
......@@ -8,6 +8,9 @@
import UIKit
fileprivate var state = SpeakEasyAssociatedKeys.UnsafeCallBack.rawValue
fileprivate var fontState = SpeakEasyAssociatedKeys.fontsize.rawValue
fileprivate var cutomFontsKey = SpeakEasyAssociatedKeys.labelTextCutomFont.rawValue
@IBDesignable
extension UIView {
......@@ -107,6 +110,26 @@ extension UIView {
}
}
@IBInspectable var fontSize:CGFloat {
set {
objc_setAssociatedObject(self, &fontState, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
let size = objc_getAssociatedObject(self, &fontState) as? CGFloat
return size ?? 12
}
}
@IBInspectable var cutomFont:Int {
set {
objc_setAssociatedObject(self, &cutomFontsKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
get {
return objc_getAssociatedObject(self, &cutomFontsKey) as? Int ?? -1
}
}
/** 获取当前控制器 */
func responderCtr() -> UIViewController? {
for view in sequence(first: self.superview, next: { $0?.superview }) {
......
//
// SpeakElePublicManager.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakElePublicManager: NSObject {
static let share = SpeakElePublicManager()
var PublicData:PublicModel = PublicModel()
func loadData() -> Void {
do {
let manager = UserDefaults.standard.object(forKey: UnsafeRawUserDefaultsKey.UnsafePublicDataKey.rawValue) as? Data ?? Data()
let data = try JSONDecoder().decode(PublicModel.self, from: manager)
PublicData = data
}catch{
}
}
func saveData() -> Void {
do{
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(PublicData)
UserDefaults.standard.set(data, forKey: UnsafeRawUserDefaultsKey.UnsafePublicDataKey.rawValue)
UserDefaults.standard.synchronize()
}catch {
}
}
private override init() { }
}
enum guideState:Int,Codable{
/// 开始
case start = 0
/// 订阅
case subscribe = 1
/// 问答
case qanda = 2
/// 登录
case login = 3
/// 首页
case home = 4
}
struct PublicModel : Codable {
var state:guideState = .start
}
......@@ -28,18 +28,18 @@ class SpeakEleGuideViewCtr: SpeakEleBaseViewCtr ,UIScrollViewDelegate {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
titleView.isHidden = true
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
@IBAction func SpeakNextTaps(_ sender: Any) {
if selectIdx >= 2 {
if let isThree = UserDefaults.standard.object(forKey: UnsafeRawUserDefaultsKey.UnsafeThreeDayGuide.rawValue) as? Bool {
let iap = SpeakEleIAPViewCtr()
iap.modalPresentationStyle = .overFullScreen
self.present(iap, animated: true)
iap.callback = { [weak self] in
let QAndA = SpeakEleQAViewCtr()
self?.navigationController?.pushViewController(QAndA, animated: true)
if isThree {
SpeakElePublicManager.share.PublicData.state = .subscribe
let iap = SpeakEleIAPViewCtr()
self.navigationController?.AnimationState = .present
self.navigationController?.pushViewController(iap, animated: true)
}
}else {
UserDefaults.standard.set(true, forKey: UnsafeRawUserDefaultsKey.UnsafeThreeDayGuide.rawValue)
......
......@@ -9,8 +9,46 @@ import UIKit
class SpeakEleQAViewCtr: SpeakEleBaseViewCtr {
// MARK: - ib
@IBOutlet weak var SpeakProgres: UISlider!
@IBOutlet weak var SpeakContentV: UIView!
@IBOutlet weak var SpeakTableView: UITableView!
@IBOutlet weak var SpeakInputBox: SpeakResizableTextView!
private let viewModel = SpeakEleQAViewModel()
@IBOutlet weak var fibkles: SpeakEleLoading!
override func viewDidLoad() {
super.viewDidLoad()
SpeakElePublicManager.share.PublicData.state = .qanda
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
titleView.isHidden = true
fibkles.startAnimating()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
SpeakContentV.cornerRect(radius: 16, [.topLeft,.topRight])
}
override func PopViewCtr() {
let navigations = self.navigationController?.viewControllers
var nav = navigations?.filter({ !($0 is SpeakEleThrDaysFreeViewCtr) })
nav?.removeLast()
if nav?.count ?? 0 > 0 {
self.navigationController?.viewControllers = nav ?? []
}else{
self.navigationController?.popViewController(animated: true)
}
}
@IBAction func SpeakSendMsg(_ sender: Any) {
}
}
......@@ -17,15 +17,9 @@ class SpeakEleThrDaysFreeViewCtr: SpeakEleBaseViewCtr {
}
private func toIAP() -> Void {
SpeakElePublicManager.share.PublicData.state = .subscribe
let iap = SpeakEleIAPViewCtr()
iap.modalPresentationStyle = .overFullScreen
self.present(iap, animated: true)
iap.callback = { [weak self] in
let QAndA = SpeakEleQAViewCtr()
var array = self?.navigationController?.viewControllers
array?.removeLast()
array?.append(QAndA)
self?.navigationController?.viewControllers = array ?? [QAndA]
}
iap.navigationController?.AnimationState = .present
self.navigationController?.pushViewController(iap, animated: true)
}
}
//
// SpeakEleQAModel.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
enum SpeakRoleState : Int,Codable{
case speakAI = 0
case speakUser = 1
}
enum SpeakMessageType:Int,Codable {
case none = 0 //
case name = 1 // 姓名
case language = 2 // 母语
case like = 3 // 偏好的语言
case hobby = 4 // 爱好
case improving = 5 // 改善
case proficiency = 6// 目前水平
case goalDays = 7 // 目标时间
case studytime = 8 // 学习时间
case practice = 9 // 练习时间
case finish = 10 // 结束
}
struct SpeakEleQAModel : Codable {
var speakMsg:String = ""
var role:SpeakRoleState = .speakAI
var messageType:SpeakMessageType = .none
}
//
// SpeakEleQAViewModel.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakEleQAViewModel: NSObject {
var questionArrys:[SpeakEleQAModel] {
get {
return [
SpeakEleQAModel(speakMsg: "Hello! I’m Speakly, an AI tutor designed just for you. I’m here to assist with boosting your English skills." ),
SpeakEleQAModel(speakMsg: "May I know your name?" ,messageType: .name),
SpeakEleQAModel(speakMsg: "Nice to make your acquaintance, $$$ !"),
SpeakEleQAModel(speakMsg: "I have a few questions to help me get a better sense of you."),
SpeakEleQAModel(speakMsg: "What language do you speak natively?",messageType: .language),
SpeakEleQAModel(speakMsg: "Would you prefer English lessons with explanations in $$$ or in English itself?",messageType: .like),
SpeakEleQAModel(speakMsg: "Go ahead and pick your interests. You can select a maximum of 4 choices." , messageType: .hobby),
SpeakEleQAModel(speakMsg: "Which aspects of English do you want to work on improving?",messageType: .improving),
SpeakEleQAModel(speakMsg: "How would you describe your current English proficiency?",messageType: .proficiency),
SpeakEleQAModel(speakMsg: "How soon do you hope to achieve your English learning goals?",messageType: .goalDays),
SpeakEleQAModel(speakMsg: "What’s your daily target for practice?",messageType: .studytime),
SpeakEleQAModel(speakMsg: "Choose a specific time each day when you’d like to start your practice sessions.",messageType: .practice),
SpeakEleQAModel(speakMsg: "I’ll send you a reminder for your practice time every day. To ensure you don’t skip a session, please enable notifications!", messageType: .finish)
]
}
}
}
......@@ -9,17 +9,19 @@ import UIKit
class SpeakEleIAPViewCtr: UIViewController {
var callback:(()->Void)?
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.AnimationState = .none
}
@IBAction func SpeakCloseTaps(_ sender: Any) {
self.dismiss(animated: true) {
guard let call = self.callback else { return }
call()
}
let QAndA = SpeakEleQAViewCtr()
self.navigationController?.pushViewController(QAndA, animated: true)
}
}
......@@ -12,13 +12,31 @@ class SpeakEleLaunchViewCtr: SpeakEleBaseViewCtr {
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.toLogin()
self.ToPages()
}
}
private func toLogin() -> Void {
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleLoginGuideCtr())
window.switchToController(nav)
private func ToPages() -> Void {
switch SpeakElePublicManager.share.PublicData.state {
case .start:
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleLoginGuideCtr())
window.switchToController(nav)
break
case .subscribe:
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleIAPViewCtr())
window.switchToController(nav)
case .qanda:
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleQAViewCtr())
window.switchToController(nav)
case .login:
break
default:
let nav = SpeakEleBaseNavigationCtr(rootViewController: SpeakEleTabbarViewCtr())
window.switchToController(nav)
break
}
}
}
......@@ -12,19 +12,27 @@ import YYText
class SpeakEleLoginGuideCtr: SpeakEleBaseViewCtr {
@IBOutlet weak var SpeakDescLabel: UILabel!
@IBOutlet weak var speakLetsGo: SpeakEleButton!
@IBOutlet weak var speakLoginBtn: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
SpeakDescLabel.setCutomFonts()
speakLetsGo.setCutomFonts()
speakLoginBtn.setCutomFonts()
setDescription()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
@IBAction func SpeakLogin(_ sender: Any) {
let login = SpeakEleCreatAViewCtr()
let login = SpeakEleLoginViewCtr()
self.navigationController?.pushViewController(login, animated: true)
}
......
......@@ -12,6 +12,8 @@
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SpeakEleLoginGuideCtr" customModule="SpeakEasyLearnEnglish" customModuleProvider="target">
<connections>
<outlet property="SpeakDescLabel" destination="jGO-Du-a3k" id="mxV-nA-Nwk"/>
<outlet property="speakLetsGo" destination="Zwf-M5-Hrt" id="GqW-aT-1s1"/>
<outlet property="speakLoginBtn" destination="GOS-gl-Ilh" id="951-M8-75o"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
......
//
// SpeakEleLoginViewCtr.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakEleLoginViewCtr: SpeakEleBaseViewCtr {
override func viewDidLoad() {
super.viewDidLoad()
}
}
//
// SpeakEleAnimation.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakCustomPushAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.35
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toVC = transitionContext.viewController(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
containerView.addSubview(toVC.view)
toVC.view.frame = CGRect(x: 0, y: containerView.bounds.height,
width: containerView.bounds.width, height: containerView.bounds.height)
UIView.animate(withDuration: transitionDuration(using: transitionContext),
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.5,
options: .curveEaseInOut) {
toVC.view.frame = containerView.bounds
fromVC.view.frame = CGRect(x: 0, y: 0,
width: containerView.bounds.width, height: containerView.bounds.height)
} completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
//
// SpeakEleLoading.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
@IBDesignable
class SpeakEleLoading : UIView {
// MARK: - 公开属性
/// 圆点颜色
@IBInspectable var dotColor: UIColor = .gray {
didSet { dots.forEach { $0.backgroundColor = dotColor } }
}
/// 圆点大小
@IBInspectable var dotSize: CGFloat = 8 {
didSet { updateDotFrames() }
}
/// 圆点间距
@IBInspectable var spacing: CGFloat = 8 {
didSet { updateDotFrames() }
}
/// 动画持续时间(秒)
@IBInspectable var animationDuration: TimeInterval = 0.4
var contentInsets:UIEdgeInsets = UIEdgeInsets()
// MARK: - 私有属性
private var dots: [UIView] = []
private var isAnimating = false
// MARK: - 初始化
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
// MARK: - 视图设置
private func setup() {
NotificationCenter.default.addObserver(
self,
selector: #selector(didBecomeActive(_:)),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(willEnterForeground(_:)),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
// 创建三个圆点
for _ in 0..<3 {
let dot = UIView()
dot.backgroundColor = dotColor
dot.layer.cornerRadius = dotSize / 2
dot.alpha = 0.3
addSubview(dot)
dots.append(dot)
}
updateDotFrames()
}
private func updateDotFrames() {
guard dots.count == 3 else { return }
let totalWidth = dotSize * 3 + spacing * 2
let startX = (bounds.width - totalWidth) / 2
for (index, dot) in dots.enumerated() {
let x = startX + CGFloat(index) * (dotSize + spacing)
dot.frame = CGRect(x: x, y: (bounds.height - dotSize) / 2, width: dotSize, height: dotSize)
}
}
// MARK: - 动画控制
func startAnimating() {
guard !isAnimating else { return }
isAnimating = true
for (index, dot) in dots.enumerated() {
let delay = Double(index) * animationDuration / Double(dots.count)
animateDot(dot, withDelay: delay)
}
}
func stopAnimating() {
isAnimating = false
dots.forEach { $0.layer.removeAllAnimations() }
dots.forEach { $0.alpha = 0.3 }
}
private func animateDot(_ dot: UIView, withDelay delay: TimeInterval) {
// 创建透明度动画
let pulseAnimation = CABasicAnimation(keyPath: "opacity")
pulseAnimation.fromValue = 0.3
pulseAnimation.toValue = 1.0
pulseAnimation.duration = animationDuration
pulseAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
pulseAnimation.repeatCount = .infinity
pulseAnimation.autoreverses = true
pulseAnimation.beginTime = CACurrentMediaTime() + delay
dot.layer.add(pulseAnimation, forKey: "pulse")
}
// MARK: - 布局
override func layoutSubviews() {
super.layoutSubviews()
updateDotFrames()
}
@objc private func didBecomeActive(_ notication:Notification) -> Void {
self.startAnimating()
}
@objc private func willEnterForeground(_ notication:Notification) -> Void {
self.stopAnimating()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
//
// SpeakKeyboardManager.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakKeyboardManager: NSObject {
static let share = SpeakKeyboardManager()
func monitor() -> Void {
// 监听键盘事件
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow(_:)),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillHide(_:)),
name: UIResponder.keyboardWillHideNotification,
object: nil
)
}
// MARK: - 键盘处理
@objc private func keyboardWillShow(_ notification: Notification) {
guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect ,
let view = notification.object as? UIView else { return }
// let keyboardTop = window.convert(keyboardFrame, from: nil).minY
//
// let textViewBottom = window.convert(bounds, from: superview).maxY
// let overlap = textViewBottom - keyboardTop + 10 // 额外间距
let overlap = keyboardFrame.height - window.safeAreaInsets.bottom
if overlap > 0 {
var frame = window.frame
frame.origin.y -= overlap
window.frame = frame
}
}
@objc private func keyboardWillHide(_ notification: Notification) {
window.frame = UIScreen.main.bounds
}
private override init() {
}
}
extension UIWindow {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let frame = self.frame
if frame.origin.y < 0 {
self.endEditing(false)
return nil
}else{
return super.hitTest(point, with: event)
}
}
}
//
// SpeakSendMsgBox.swift
// SpeakEasyLearnEnglish
//
// Created by edy on 2025/7/11.
//
import UIKit
class SpeakResizableTextView: UIView {
// MARK: - 公开属性
/// 文本内容
@IBInspectable var text: String? {
get { return textView.text }
set { textView.text = newValue }
}
/// 占位符文本
@IBInspectable var placeholder: String? {
get { return placeholderLabel.text }
set { placeholderLabel.text = newValue }
}
/// 占位符颜色
@IBInspectable var placeholderColor: UIColor = .lightGray {
didSet { placeholderLabel.textColor = placeholderColor }
}
/// 文本颜色
@IBInspectable var textColor: UIColor? {
get { return textView.textColor }
set { textView.textColor = newValue }
}
/// 字体
var font: UIFont? {
get { return textView.font }
set { textView.font = newValue }
}
/// 最大行数限制(0 表示无限制)
@IBInspectable var maxLines: Int = 0 {
didSet { updateMaxHeight() }
}
/// 文本变化回调
var onTextChanged: ((String) -> Void)?
/// 达到最大行数回调
var onReachMaxLines: (() -> Void)?
// MARK: - 私有属性
private let textView = UITextView()
private let placeholderLabel = UILabel()
private var maxHeight: CGFloat = .greatestFiniteMagnitude
private var originalInset: UIEdgeInsets?
// MARK: - 初始化
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupConstraints()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupViews()
setupConstraints()
}
// MARK: - 视图设置
private func setupViews() {
// 设置背景和边框
backgroundColor = .white
layer.cornerRadius = 8
layer.borderWidth = 1
layer.borderColor = UIColor.lightGray.cgColor
// 配置 UITextView
textView.delegate = self
textView.isScrollEnabled = false
textView.backgroundColor = .clear
textView.font = UIFont.systemFont(ofSize: 16)
textView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
textView.keyboardDismissMode = .onDrag
addSubview(textView)
// 配置占位符标签
placeholderLabel.textColor = placeholderColor
placeholderLabel.font = textView.font
placeholderLabel.numberOfLines = 0
placeholderLabel.isUserInteractionEnabled = false
addSubview(placeholderLabel)
}
private func setupConstraints() {
textView.translatesAutoresizingMaskIntoConstraints = false
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
textView.snp.makeConstraints { make in
make.left.top.bottom.right.equalToSuperview().inset(UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4))
make.height.greaterThanOrEqualTo(30)
}
placeholderLabel.snp.makeConstraints { make in
make.top.bottom.equalToSuperview().inset(8)
make.left.equalToSuperview().inset(16)
}
}
// MARK: - 高度计算和更新
private func updateHeight() {
let size = CGSize(width: textView.bounds.width - textView.textContainerInset.left - textView.textContainerInset.right,
height: .greatestFiniteMagnitude)
let estimatedSize = textView.sizeThatFits(size)
let newHeight = min(estimatedSize.height, maxHeight)
var frame = self.frame
frame.size.height = newHeight
self.frame = frame
placeholderLabel.isHidden = !textView.text.isEmpty
onTextChanged?(textView.text)
}
private func updateMaxHeight() {
if maxLines > 0, let font = textView.font {
let lineHeight = font.lineHeight
maxHeight = lineHeight * CGFloat(maxLines) + textView.textContainerInset.top + textView.textContainerInset.bottom
} else {
maxHeight = .greatestFiniteMagnitude
}
}
private func findScrollView() -> UIScrollView? {
var view: UIView? = self
while let superview = view?.superview {
if let scrollView = superview as? UIScrollView {
if originalInset == nil {
originalInset = scrollView.contentInset
}
return scrollView
}
view = superview
}
return nil
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
// MARK: - UITextViewDelegate
extension SpeakResizableTextView : UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
updateHeight()
if maxLines > 0 {
let size = CGSize(width: textView.bounds.width - textView.textContainerInset.left - textView.textContainerInset.right,
height: .greatestFiniteMagnitude)
let estimatedSize = textView.sizeThatFits(size)
if estimatedSize.height >= maxHeight {
onReachMaxLines?()
}
}
}
func textViewDidBeginEditing(_ textView: UITextView) {
placeholderLabel.isHidden = !textView.text.isEmpty
}
func textViewDidEndEditing(_ textView: UITextView) {
placeholderLabel.isHidden = !textView.text.isEmpty
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment